From fa2bb59ab333e22c38393f0b817ea51207a62e98 Mon Sep 17 00:00:00 2001 From: lefarcen <935902669@qq.com> Date: Wed, 13 May 2026 18:49:44 +0800 Subject: [PATCH] fix(test): align FileViewer tests with merged component - Drop FileViewer.inspect-empty-hint.test.tsx (deleted on main; should have been gone after the merge but the modify/delete conflict left it in the tree). - Take main's FileViewer.test.tsx so the manual-edit interaction tests match the merged FileViewer.tsx behaviour, then patch every render with projectKind="prototype" so it satisfies the prop requirement release added in #1509. --- .../FileViewer.inspect-empty-hint.test.tsx | 164 ------------------ apps/web/tests/components/FileViewer.test.tsx | 152 ++++++++-------- 2 files changed, 84 insertions(+), 232 deletions(-) delete mode 100644 apps/web/tests/components/FileViewer.inspect-empty-hint.test.tsx diff --git a/apps/web/tests/components/FileViewer.inspect-empty-hint.test.tsx b/apps/web/tests/components/FileViewer.inspect-empty-hint.test.tsx deleted file mode 100644 index af1060b773..0000000000 --- a/apps/web/tests/components/FileViewer.inspect-empty-hint.test.tsx +++ /dev/null @@ -1,164 +0,0 @@ -// @vitest-environment jsdom - -import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'; -import { afterEach, describe, expect, it } from 'vitest'; -import { FileViewer } from '../../src/components/FileViewer'; -import type { ProjectFile } from '../../src/types'; - -// Regression coverage for nexu-io/open-design#890. When the agent emits -// an HTML artifact with no `data-od-id` / `data-screen-label` -// annotations (a freeform PRD → HTML pass without going through a -// skill, for example), the existing inspect-empty-hint banner lied: -// it said "Click any element with `data-od-id` to tune its style" -// even though no element on the page carried that attribute. The user -// would click, the bridge's click handler would walk up to , -// find nothing tagged, and silently bail — no UI feedback. This test -// pins the new dispatch: -// -// - liveCommentTargets.size === 0 → empty-state copy explaining -// what's missing and how to fix it. -// - liveCommentTargets.size > 0 → existing instruction copy. -// -// And mirrors the same affordance into Picker mode so both inspect -// surfaces give the user the same calibration signal. - -function htmlFile(name = 'mock.html'): ProjectFile { - return { - name, - path: name, - type: 'file', - size: 1024, - mtime: 1710000000, - kind: 'html', - mime: 'text/html', - artifactManifest: { - version: 1, - kind: 'html', - title: 'Mock', - entry: name, - renderer: 'html', - exports: ['html'], - }, - }; -} - -function postTargetsFromIframe(targets: Array<{ elementId: string }>): void { - // FileViewer's message handler keys off `ev.source === - // iframeRef.current?.contentWindow` to defend against cross-frame - // chatter on the host. Find the host's iframe in the rendered DOM - // and dispatch a real MessageEvent with `source` set to its - // contentWindow so the handler accepts the payload. - const iframe = document.querySelector('iframe'); - if (!iframe) throw new Error('iframe not in DOM yet'); - const event = new MessageEvent('message', { - data: { - type: 'od:comment-targets', - targets: targets.map((t) => ({ - elementId: t.elementId, - selector: `[data-od-id="${t.elementId}"]`, - label: 'div', - text: '', - position: { x: 0, y: 0, width: 100, height: 50 }, - htmlHint: '', - })), - }, - source: iframe.contentWindow, - }); - window.dispatchEvent(event); -} - -describe('FileViewer Inspect/Picker empty-annotation hint (#890)', () => { - afterEach(() => { - cleanup(); - }); - - it('shows the empty-state copy in Inspect mode when the iframe reports zero annotated targets', async () => { - render( - , - ); - - // Activate Inspect — the toolbar button has a stable test id from - // the surrounding viewer chrome. - fireEvent.click(screen.getByTestId('inspect-mode-toggle')); - - // The bridge boot sequence ends with a `od:comment-targets` post - // carrying an empty array for unannotated artifacts (pinned in - // tests/runtime/srcdoc-bridge-empty-targets.test.ts). Simulate - // that broadcast and assert the host renders the empty-state - // copy instead of the instructive default. - await act(async () => { - postTargetsFromIframe([]); - }); - - expect(screen.queryByTestId('inspect-empty-hint-no-targets')).toBeTruthy(); - expect(screen.queryByTestId('inspect-empty-hint')).toBeNull(); - // The message must mention what's missing so the user knows what - // to ask the agent for. We assert on the attribute name verbatim - // — it's the exact token the agent has to add to the artifact. - expect(screen.getByTestId('inspect-empty-hint-no-targets').textContent ?? '') - .toMatch(/data-od-id/); - }); - - it('switches back to the instructive copy once the iframe reports at least one annotated target', async () => { - render( - , - ); - - fireEvent.click(screen.getByTestId('inspect-mode-toggle')); - - await act(async () => { - postTargetsFromIframe([{ elementId: 'hero' }]); - }); - - // Existing copy survives — pinning that the new dispatch doesn't - // accidentally drop the long-standing affordance for users whose - // artifacts already ship annotations. - expect(screen.getByTestId('inspect-empty-hint')).toBeTruthy(); - expect(screen.queryByTestId('inspect-empty-hint-no-targets')).toBeNull(); - }); - - it('shows the empty-state copy in Picker mode when the iframe reports zero annotated targets', async () => { - // Picker mode (Comments → Picker tool) has the same failure - // surface as Inspect: clicking an unannotated element no-ops. - // This test mirrors the Inspect coverage above. - render( - , - ); - - // Tweaks mode boots with the Picker tool already selected - // (`boardTool` defaults to `'inspect'`), so the empty-state hint - // path fires the moment we enter Tweaks — no inner button click - // needed. The inner `comment-mode-toggle` only renders alongside - // its Pods sibling once Tweaks is on. - fireEvent.click(screen.getByTestId('board-mode-toggle')); - - await act(async () => { - postTargetsFromIframe([]); - }); - - // The same testid surfaces the empty-state copy regardless of - // which inspect surface is active — keeps the i18n-free copy - // consolidated and makes future migration to translated strings - // a single edit instead of two. - expect(screen.queryByTestId('inspect-empty-hint-no-targets')).toBeTruthy(); - // Copy is mode-aware so the action is concrete: in Picker mode - // the user is leaving comments, not tuning style. - expect(screen.getByTestId('inspect-empty-hint-no-targets').textContent ?? '') - .toMatch(/comment on/i); - }); -}); diff --git a/apps/web/tests/components/FileViewer.test.tsx b/apps/web/tests/components/FileViewer.test.tsx index 9ebc6aa541..10f90537d2 100644 --- a/apps/web/tests/components/FileViewer.test.tsx +++ b/apps/web/tests/components/FileViewer.test.tsx @@ -1,5 +1,7 @@ // @vitest-environment jsdom +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { renderToStaticMarkup } from 'react-dom/server'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -375,10 +377,7 @@ describe('FileViewer SVG artifacts', () => { }); const markup = renderToStaticMarkup( -
one
'} />, @@ -406,10 +405,7 @@ describe('FileViewer SVG artifacts', () => { }); const markup = renderToStaticMarkup( -
one
two
'} />, ); @@ -435,10 +431,7 @@ describe('FileViewer SVG artifacts', () => { }); render( - , ); @@ -518,10 +511,7 @@ describe('FileViewer SVG artifacts', () => { vi.stubGlobal('fetch', fetchMock); render( - , ); @@ -583,10 +573,7 @@ describe('FileViewer SVG artifacts', () => { vi.stubGlobal('fetch', fetchMock); render( - , ); @@ -712,10 +699,7 @@ describe('FileViewer SVG artifacts', () => { vi.stubGlobal('fetch', fetchMock); render( - , ); @@ -806,10 +790,7 @@ describe('FileViewer SVG artifacts', () => { }); render( - , ); @@ -856,10 +837,7 @@ describe('FileViewer SVG artifacts', () => { }), { status: 200 }))); render( - , ); @@ -921,10 +899,7 @@ describe('FileViewer SVG artifacts', () => { }); render( - , ); @@ -950,7 +925,7 @@ describe('FileViewer SVG artifacts', () => { }); }); -describe('FileViewer comment picker and tweaks mode', () => { +describe('FileViewer tweaks toolbar', () => { function htmlPreviewFile(): ProjectFile { return baseFile({ name: 'preview.html', @@ -968,54 +943,85 @@ describe('FileViewer comment picker and tweaks mode', () => { }); } - it('turns off tweaks when manual edit is enabled so picker actions disappear', () => { + it('renders the toolbar Draw entry and no legacy picker/pod toggle', () => { render( - , ); - const tweaksToggle = screen.getByTestId('board-mode-toggle'); - const manualEditToggle = screen.getByTestId('manual-edit-mode-toggle'); - - fireEvent.click(tweaksToggle); - expect(tweaksToggle.getAttribute('aria-pressed')).toBe('true'); - expect(screen.getByTestId('comment-mode-toggle')).toBeTruthy(); - expect(screen.getByRole('button', { name: 'Pods' })).toBeTruthy(); - - fireEvent.click(manualEditToggle); - - expect(manualEditToggle.getAttribute('aria-pressed')).toBe('true'); - expect(tweaksToggle.getAttribute('aria-pressed')).toBe('false'); + expect(screen.getByTestId('palette-tweaks-toggle')).toBeTruthy(); + expect(screen.getByTestId('draw-overlay-toggle')).toBeTruthy(); + expect(screen.queryByPlaceholderText('Type anywhere to add a note')).toBeNull(); + expect(screen.queryByTestId('board-mode-toggle')).toBeNull(); expect(screen.queryByTestId('comment-mode-toggle')).toBeNull(); expect(screen.queryByRole('button', { name: 'Pods' })).toBeNull(); + expect(screen.queryByTestId('inspect-mode-toggle')).toBeNull(); + expect(screen.queryByRole('button', { name: 'Inspect' })).toBeNull(); + + fireEvent.click(screen.getByTestId('draw-overlay-toggle')); + expect(screen.getByPlaceholderText('Type anywhere to add a note')).toBeTruthy(); + expect(screen.getByRole('button', { name: 'Click' })).toBeTruthy(); + + fireEvent.click(screen.getByTestId('draw-overlay-toggle')); + expect(screen.queryByPlaceholderText('Type anywhere to add a note')).toBeNull(); }); - it('turns off manual edit when tweaks are enabled from edit mode', () => { + it('keeps the Draw bar open after queueing an annotation', () => { render( - , ); - const tweaksToggle = screen.getByTestId('board-mode-toggle'); - const manualEditToggle = screen.getByTestId('manual-edit-mode-toggle'); + fireEvent.click(screen.getByTestId('draw-overlay-toggle')); + const note = screen.getByPlaceholderText('Type anywhere to add a note'); + fireEvent.change(note, { target: { value: 'mark this' } }); + fireEvent.click(screen.getByRole('button', { name: 'Queue' })); - fireEvent.click(manualEditToggle); - expect(manualEditToggle.getAttribute('aria-pressed')).toBe('true'); + expect(screen.getByPlaceholderText('Type anywhere to add a note')).toBeTruthy(); + expect(screen.getAllByRole('button', { name: 'Draw' })[1]?.getAttribute('aria-pressed')).toBe('true'); + expect(screen.getByRole('button', { name: 'Click' }).getAttribute('aria-pressed')).toBe('false'); - fireEvent.click(tweaksToggle); + fireEvent.click(screen.getByTestId('draw-overlay-toggle')); + expect(screen.queryByPlaceholderText('Type anywhere to add a note')).toBeNull(); + }); - expect(tweaksToggle.getAttribute('aria-pressed')).toBe('true'); - expect(manualEditToggle.getAttribute('aria-pressed')).toBe('false'); - expect(screen.getByTestId('comment-mode-toggle')).toBeTruthy(); - expect(screen.getByRole('button', { name: 'Pods' })).toBeTruthy(); + it('enables element picking while the Draw bar is in click mode', async () => { + render( + , + ); + + const frame = screen.getByTestId('artifact-preview-frame') as HTMLIFrameElement; + fireEvent.click(screen.getByTestId('draw-overlay-toggle')); + await waitFor(() => expect(frame.srcdoc).not.toContain('data-od-selection-bridge')); + + fireEvent.click(screen.getByRole('button', { name: 'Click' })); + + await waitFor(() => expect(frame.srcdoc).toContain('data-od-selection-bridge')); + expect(frame.srcdoc).toContain('data-od-comment-mode'); + }); + + it('disables Draw direct send while a task is running but keeps queue available', () => { + render( + , + ); + + fireEvent.click(screen.getByTestId('draw-overlay-toggle')); + fireEvent.change(screen.getByPlaceholderText('Type anywhere to add a note'), { + target: { value: 'mark this' }, + }); + + const queue = screen.getByRole('button', { name: 'Queue' }) as HTMLButtonElement; + expect(queue.disabled).toBe(false); + const send = screen.getByRole('button', { name: 'Send (当前正有任务在执行)' }) as HTMLButtonElement; + expect(send.disabled).toBe(true); + expect(send.getAttribute('title')).toBe('当前正有任务在执行'); }); }); @@ -1568,6 +1574,16 @@ function baseLiveArtifactWorkspaceEntry( } describe('LiveArtifactViewer', () => { + it('keeps the presentation exit button aligned with preview chrome spacing', () => { + const css = readFileSync(join(process.cwd(), 'src/index.css'), 'utf8'); + const rule = css.match(/\.present-exit\s*\{[^}]+\}/)?.[0] ?? ''; + + expect(rule).toContain('top: calc(env(safe-area-inset-top, 0px) + 20px);'); + expect(rule).toContain('right: calc(env(safe-area-inset-right, 0px) + 20px);'); + expect(rule).toContain('display: inline-flex;'); + expect(rule).toContain('align-items: center;'); + }); + it('enters and exits in-tab presentation from the present menu', async () => { const fetchMock = vi.fn(async (input: string | URL | Request) => { const url = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);