From 7973b02edcc119acb936d2b6faeb17b0bcbf78ec Mon Sep 17 00:00:00 2001 From: Jingyi-Dify Date: Tue, 7 Jul 2026 18:16:06 -0700 Subject: [PATCH] fix(web): stabilize learn dify tour target --- .../explore/app-list/__tests__/index.spec.tsx | 6 ++ web/app/components/explore/app-list/index.tsx | 3 + .../__tests__/mount.spec.tsx | 57 +++++++++++++++++++ .../step-by-step-tour/use-tour-target.ts | 2 + 4 files changed, 68 insertions(+) diff --git a/web/app/components/explore/app-list/__tests__/index.spec.tsx b/web/app/components/explore/app-list/__tests__/index.spec.tsx index dded1f6781d..ead7ee3a658 100644 --- a/web/app/components/explore/app-list/__tests__/index.spec.tsx +++ b/web/app/components/explore/app-list/__tests__/index.spec.tsx @@ -888,6 +888,7 @@ describe('AppList', () => { mockStepByStepTour.setUiState({ activeTaskId: 'home', activeGuideIndex: 0, + activeGuideIndexes: [0, 1], minimized: true, }) @@ -931,6 +932,7 @@ describe('AppList', () => { expect(state?.activeTaskId).toBeUndefined() expect(state?.activeGuideIndex).toBeUndefined() expect(state?.activeGuideGroup).toBeUndefined() + expect(state?.activeGuideIndexes).toBeUndefined() expect(state?.completedTaskIds).toEqual(['home']) expect(state?.minimized).toBe(false) }) @@ -945,6 +947,7 @@ describe('AppList', () => { mockStepByStepTour.setUiState({ activeTaskId: 'home', activeGuideIndex: 0, + activeGuideIndexes: [0, 1], minimized: true, }); (fetchAppDetail as unknown as Mock).mockResolvedValue({ export_data: 'yaml-content', mode: AppModeEnum.CHAT }) @@ -962,6 +965,7 @@ describe('AppList', () => { const state = mockStepByStepTour.observedState expect(state?.activeTaskId).toBeUndefined() expect(state?.activeGuideIndex).toBeUndefined() + expect(state?.activeGuideIndexes).toBeUndefined() expect(state?.completedTaskIds).toEqual(['home']) }) expect(mockHandleImportDSL).toHaveBeenCalledWith( @@ -1016,6 +1020,7 @@ describe('AppList', () => { mockStepByStepTour.setUiState({ activeTaskId: 'home', activeGuideIndex: 0, + activeGuideIndexes: [0, 1], minimized: true, }) @@ -1038,6 +1043,7 @@ describe('AppList', () => { const state = mockStepByStepTour.observedState expect(state?.activeTaskId).toBeUndefined() expect(state?.activeGuideIndex).toBeUndefined() + expect(state?.activeGuideIndexes).toBeUndefined() expect(state?.completedTaskIds).toEqual([]) }) expect(screen.queryByTestId('try-app-panel')).not.toBeInTheDocument() diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx index 1305d0cac05..904e5ca7c49 100644 --- a/web/app/components/explore/app-list/index.tsx +++ b/web/app/components/explore/app-list/index.tsx @@ -267,6 +267,7 @@ const Apps = ({ onSuccess }: { onSuccess?: () => void }) => { activeTaskId: undefined, activeGuideIndex: undefined, activeGuideGroup: undefined, + activeGuideIndexes: undefined, minimized: true, }) }, [setStepByStepTourAccountState, stepByStepTourAccountState]) @@ -280,6 +281,7 @@ const Apps = ({ onSuccess }: { onSuccess?: () => void }) => { activeTaskId: undefined, activeGuideIndex: undefined, activeGuideGroup: undefined, + activeGuideIndexes: undefined, minimized: false, }) stepByStepTourActions.completeTask(HOME_STEP_BY_STEP_TOUR_TASK_ID, { @@ -304,6 +306,7 @@ const Apps = ({ onSuccess }: { onSuccess?: () => void }) => { activeTaskId: undefined, activeGuideIndex: undefined, activeGuideGroup: undefined, + activeGuideIndexes: undefined, minimized: false, }) stepByStepTourActions.completeTask(HOME_STEP_BY_STEP_TOUR_TASK_ID, { diff --git a/web/app/components/step-by-step-tour/__tests__/mount.spec.tsx b/web/app/components/step-by-step-tour/__tests__/mount.spec.tsx index df4ebdf50a4..7b001af35ed 100644 --- a/web/app/components/step-by-step-tour/__tests__/mount.spec.tsx +++ b/web/app/components/step-by-step-tour/__tests__/mount.spec.tsx @@ -1190,6 +1190,63 @@ describe('StepByStepTourMount', () => { } }) + it('renders the coachmark when the target attribute is added to an existing element', async () => { + const animationFrameCallbacks: FrameRequestCallback[] = [] + const requestAnimationFrameSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => { + animationFrameCallbacks.push(callback) + return animationFrameCallbacks.length + }) + const cancelAnimationFrameSpy = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {}) + setStepByStepTourTestState({ + activeTaskId: 'home', + activeGuideIndex: 1, + manuallyEnabledWorkspaceIds: ['workspace-1'], + manuallyDisabledWorkspaceIds: [], + minimized: true, + completedTaskIds: [], + skipped: false, + }) + const target = document.createElement('button') + target.type = 'button' + target.textContent = 'Create from sample app' + target.getBoundingClientRect = () => ({ + bottom: 280, + height: 40, + left: 980, + right: 1300, + top: 240, + width: 320, + x: 980, + y: 240, + toJSON: () => ({}), + }) + document.body.appendChild(target) + const flushAnimationFrames = () => { + act(() => { + animationFrameCallbacks.splice(0).forEach(callback => callback(0)) + }) + } + + try { + renderStepByStepTourMount() + flushAnimationFrames() + expect(screen.queryByText('Click here to make it yours')).not.toBeInTheDocument() + + target.dataset.stepByStepTourTarget = STEP_BY_STEP_TOUR_TARGETS.homeTryAppCreate + await act(async () => { + await Promise.resolve() + }) + flushAnimationFrames() + + expect(await screen.findByText('Click here to make it yours')).toBeInTheDocument() + } + finally { + requestAnimationFrameSpy.mockRestore() + cancelAnimationFrameSpy.mockRestore() + target.remove() + } + }) + it('measures the coachmark size without observing the coachmark element', async () => { const resizeObservers: Array<{ observedElements: Element[] diff --git a/web/app/components/step-by-step-tour/use-tour-target.ts b/web/app/components/step-by-step-tour/use-tour-target.ts index bead080f84a..154da9e66b2 100644 --- a/web/app/components/step-by-step-tour/use-tour-target.ts +++ b/web/app/components/step-by-step-tour/use-tour-target.ts @@ -45,6 +45,8 @@ export const useStepByStepTourTarget = (target?: string) => { const observer = new MutationObserver(scheduleSyncTarget) observer.observe(document.body, { + attributeFilter: ['data-step-by-step-tour-target'], + attributes: true, childList: true, subtree: true, })