mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
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 <FileViewer> render with projectKind="prototype" so it satisfies the prop requirement release added in #1509.
This commit is contained in:
@@ -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 <html>,
|
||||
// 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(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={htmlFile()}
|
||||
liveHtml="<html><body><h1>Plain PRD with no data-od-id</h1></body></html>"
|
||||
/>,
|
||||
);
|
||||
|
||||
// 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(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={htmlFile()}
|
||||
liveHtml="<html><body><main data-od-id='hero'>Hero</main></body></html>"
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={htmlFile()}
|
||||
liveHtml="<html><body><h1>No annotations</h1></body></html>"
|
||||
/>,
|
||||
);
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={file}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={file}
|
||||
isDeck
|
||||
liveHtml={'<html><body><section class="slide">one</section></body></html>'}
|
||||
/>,
|
||||
@@ -406,10 +405,7 @@ describe('FileViewer SVG artifacts', () => {
|
||||
});
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={file}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={file}
|
||||
liveHtml={'<html><body><section class="slide">one</section><section class="slide">two</section></body></html>'}
|
||||
/>,
|
||||
);
|
||||
@@ -435,10 +431,7 @@ describe('FileViewer SVG artifacts', () => {
|
||||
});
|
||||
|
||||
render(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={file}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={file}
|
||||
liveHtml="<html><body><h1>Hello</h1></body></html>"
|
||||
/>,
|
||||
);
|
||||
@@ -518,10 +511,7 @@ describe('FileViewer SVG artifacts', () => {
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
render(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={file}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={file}
|
||||
liveHtml="<html><body><h1>Hello</h1></body></html>"
|
||||
/>,
|
||||
);
|
||||
@@ -583,10 +573,7 @@ describe('FileViewer SVG artifacts', () => {
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
render(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={file}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={file}
|
||||
liveHtml="<html><body><h1>Hello</h1></body></html>"
|
||||
/>,
|
||||
);
|
||||
@@ -712,10 +699,7 @@ describe('FileViewer SVG artifacts', () => {
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
render(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={file}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={file}
|
||||
liveHtml="<html><body><h1>Hello</h1></body></html>"
|
||||
/>,
|
||||
);
|
||||
@@ -806,10 +790,7 @@ describe('FileViewer SVG artifacts', () => {
|
||||
});
|
||||
|
||||
render(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={file}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={file}
|
||||
liveHtml="<html><body><h1>Hello</h1></body></html>"
|
||||
/>,
|
||||
);
|
||||
@@ -856,10 +837,7 @@ describe('FileViewer SVG artifacts', () => {
|
||||
}), { status: 200 })));
|
||||
|
||||
render(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={file}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={file}
|
||||
liveHtml="<html><body><h1>Hello</h1></body></html>"
|
||||
/>,
|
||||
);
|
||||
@@ -921,10 +899,7 @@ describe('FileViewer SVG artifacts', () => {
|
||||
});
|
||||
|
||||
render(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={file}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={file}
|
||||
liveHtml="<html><body><h1>Hello</h1></body></html>"
|
||||
/>,
|
||||
);
|
||||
@@ -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(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={htmlPreviewFile()}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={htmlPreviewFile()}
|
||||
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FileViewer
|
||||
projectId="project-1"
|
||||
projectKind="prototype"
|
||||
file={htmlPreviewFile()}
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={htmlPreviewFile()}
|
||||
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={htmlPreviewFile()}
|
||||
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<FileViewer projectId="project-1" projectKind="prototype" file={htmlPreviewFile()}
|
||||
liveHtml='<html><body><main data-od-id="hero">Hero</main></body></html>'
|
||||
streaming
|
||||
/>,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user