Skip to content

Commit 49df373

Browse files
authored
fix: update the guide as a new object if updated its step (#769)
1 parent 0636e63 commit 49df373

File tree

3 files changed

+149
-4
lines changed

3 files changed

+149
-4
lines changed

.changeset/some-singers-yell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@knocklabs/client": patch
3+
---
4+
5+
update guide as a new object ref when updating its step

packages/client/src/clients/guide/client.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ export class KnockGuideClient {
685685

686686
switch (this.stage.status) {
687687
case "open": {
688-
this.knock.log(`[Guide] Addng to the group stage: ${guide.key}`);
688+
this.knock.log(`[Guide] Adding to the group stage: ${guide.key}`);
689689
this.stage.ordered[index] = guide.key;
690690
return undefined;
691691
}
@@ -1033,7 +1033,7 @@ export class KnockGuideClient {
10331033
}
10341034

10351035
this.store.setState((state) => {
1036-
const guide = state.guides[guideKey];
1036+
let guide = state.guides[guideKey];
10371037
if (!guide) return state;
10381038

10391039
const steps = guide.steps.map((step) => {
@@ -1046,8 +1046,9 @@ export class KnockGuideClient {
10461046

10471047
return step;
10481048
});
1049-
// Mutate in place and maintain the same obj ref.
1050-
guide.steps = steps;
1049+
// If updated, return the guide as a new object so useStore can trigger.
1050+
guide = updatedStep ? { ...guide, steps } : guide;
1051+
10511052
const guides = { ...state.guides, [guide.key]: guide };
10521053

10531054
// If the guide is subject to throttled settings and we are marking as

packages/client/test/clients/guide/guide.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,41 @@ describe("KnockGuideClient", () => {
646646
);
647647
});
648648

649+
test("marks guide step as interacted and returns new guide object", async () => {
650+
const client = new KnockGuideClient(mockKnock, channelId);
651+
652+
// Mock the store to have the guide so setStepMessageAttrs can find it
653+
const stateWithGuides = {
654+
guideGroups: [mockDefaultGroup],
655+
guideGroupDisplayLogs: {},
656+
guides: { [mockGuide.key]: mockGuide },
657+
previewGuides: {},
658+
queries: {},
659+
location: undefined,
660+
counter: 0,
661+
debug: { forcedGuideKey: null, previewSessionId: null },
662+
};
663+
mockStore.state = stateWithGuides;
664+
mockStore.getState.mockReturnValue(stateWithGuides);
665+
666+
// Store the original guide reference
667+
const originalGuideRef = mockStore.state.guides[mockGuide.key];
668+
669+
await client.markAsInteracted(mockGuide, mockStep, { action: "clicked" });
670+
671+
// Get the setState function and execute it to verify the state changes
672+
const setStateCalls = mockStore.setState.mock.calls;
673+
const stateUpdateFn = setStateCalls.find(
674+
(call) => typeof call[0] === "function",
675+
)?.[0];
676+
677+
const newState = stateUpdateFn(stateWithGuides);
678+
679+
// Verify that the guide object is a new reference (not the same object)
680+
// This ensures useStore triggers a re-render
681+
expect(newState.guides[mockGuide.key]).not.toBe(originalGuideRef);
682+
});
683+
649684
test("marks guide step as archived", async () => {
650685
const client = new KnockGuideClient(mockKnock, channelId);
651686

@@ -2650,6 +2685,110 @@ describe("KnockGuideClient", () => {
26502685
});
26512686
});
26522687

2688+
test("setStepMessageAttrs returns guide as new object when step is updated", () => {
2689+
const mockGuide = {
2690+
key: "test_guide",
2691+
steps: [
2692+
{
2693+
ref: "step_1",
2694+
message: { id: "msg_123", seen_at: null },
2695+
},
2696+
],
2697+
} as unknown as KnockGuide;
2698+
2699+
const stateWithGuides = {
2700+
guideGroups: [],
2701+
guideGroupDisplayLogs: {},
2702+
guides: { [mockGuide.key]: mockGuide },
2703+
previewGuides: {},
2704+
queries: {},
2705+
location: undefined,
2706+
counter: 0,
2707+
debug: { forcedGuideKey: null, previewSessionId: null },
2708+
};
2709+
2710+
mockStore.state = stateWithGuides;
2711+
mockStore.getState.mockReturnValue(stateWithGuides);
2712+
2713+
const client = new KnockGuideClient(mockKnock, channelId);
2714+
2715+
// Store the original guide reference
2716+
const originalGuideRef = mockStore.state.guides[mockGuide.key];
2717+
2718+
// Update the step message attributes
2719+
client["setStepMessageAttrs"]("test_guide", "step_1", {
2720+
seen_at: "2023-01-01T00:00:00Z",
2721+
});
2722+
2723+
// Get the setState function and execute it to verify the state changes
2724+
const setStateCalls = mockStore.setState.mock.calls;
2725+
const stateUpdateFn = setStateCalls.find(
2726+
(call) => typeof call[0] === "function",
2727+
)?.[0];
2728+
2729+
const newState = stateUpdateFn(stateWithGuides);
2730+
2731+
// Verify that the guide object is a new reference (not the same object)
2732+
expect(newState.guides["test_guide"]).not.toBe(originalGuideRef);
2733+
2734+
// Verify that the step message was updated
2735+
expect(newState.guides["test_guide"]!.steps[0]!.message.seen_at).toBe(
2736+
"2023-01-01T00:00:00Z",
2737+
);
2738+
});
2739+
2740+
test("setStepMessageAttrs returns same guide object when step is not found", () => {
2741+
const mockGuide = {
2742+
key: "test_guide",
2743+
steps: [
2744+
{
2745+
ref: "step_1",
2746+
message: { id: "msg_123", seen_at: null },
2747+
},
2748+
],
2749+
} as unknown as KnockGuide;
2750+
2751+
const stateWithGuides = {
2752+
guideGroups: [],
2753+
guideGroupDisplayLogs: {},
2754+
guides: { [mockGuide.key]: mockGuide },
2755+
previewGuides: {},
2756+
queries: {},
2757+
location: undefined,
2758+
counter: 0,
2759+
debug: { forcedGuideKey: null, previewSessionId: null },
2760+
};
2761+
2762+
mockStore.state = stateWithGuides;
2763+
mockStore.getState.mockReturnValue(stateWithGuides);
2764+
2765+
const client = new KnockGuideClient(mockKnock, channelId);
2766+
2767+
// Store the original guide reference
2768+
const originalGuideRef = mockStore.state.guides[mockGuide.key];
2769+
2770+
// Try to update a non-existent step
2771+
client["setStepMessageAttrs"]("test_guide", "non_existent_step", {
2772+
seen_at: "2023-01-01T00:00:00Z",
2773+
});
2774+
2775+
// Get the setState function and execute it to verify the state changes
2776+
const setStateCalls = mockStore.setState.mock.calls;
2777+
const stateUpdateFn = setStateCalls.find(
2778+
(call) => typeof call[0] === "function",
2779+
)?.[0];
2780+
2781+
const newState = stateUpdateFn(stateWithGuides);
2782+
2783+
// Verify that the guide object is the same reference (no update occurred)
2784+
expect(newState.guides["test_guide"]).toBe(originalGuideRef);
2785+
2786+
// Verify that the step message was NOT updated
2787+
expect(newState.guides["test_guide"]!.steps[0]!.message.seen_at).toBe(
2788+
null,
2789+
);
2790+
});
2791+
26532792
test("buildQueryParams handles missing data and tenant", () => {
26542793
const client = new KnockGuideClient(mockKnock, channelId);
26552794

0 commit comments

Comments
 (0)