mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* fix(web): dispatch Examples preview on od.preview.type (#897) The Examples gallery unconditionally fetched `/api/skills/:id/example`, and the daemon endpoint only resolves HTML files (`example.html`, `assets/template.html`, `assets/index.html`, `examples/*.html`). Skills that declare `od.preview.type: image` (`hatch-pet`) or `od.preview.type: markdown` (`dcf-valuation`, `last30days`, `x-research`) ship no such HTML — the fetch returns 404 and the modal landed on the misleading "Couldn't load this example. The example HTML failed to fetch." copy. Dispatch on `previewType` at the data layer (`fetchSkillExample`) and at the render layer (`PreviewModal`): - `fetchSkillExample(id, previewType)` short-circuits any non-`html` value to `{ unavailable: true, kind }` without firing a network call. - `PreviewView` grows an optional `unavailable: { kind }` shape; the modal renders a calm "no shipped preview" placeholder distinct from loading and error states. The Share menu disables (no HTML to export). - `ExamplesTab` tracks `previewUnavailable` per skill alongside the existing `previews` / `previewErrors` maps; the card placeholder swaps to "open to learn more" copy so users don't hover waiting for a render that won't come. - New `preview.unavailableTitle` / `preview.unavailableBody` and `examples.unavailablePlaceholder` / `examples.shareUnavailable` keys shipped across all 17 locales. Body copy uses the raw preview kind (`{kind}` placeholder) so future kinds slot in without a copy change. Tests: registry-level coverage that the dispatch never hits the network for non-html types; PreviewModal-level coverage that the unavailable affordance is mutually exclusive with loading/error and disables the Share menu; ExamplesTab-level coverage that the gallery renders the unavailable state for image/markdown skills and routes html skills through the existing fetch path. Updated the existing `#860` retry regression test for the new two-arg signature. * fix(web): use neutral noun for preview.unavailableBody copy (#1001 review) P3 from lefarcen: `'a {kind} document'` reads awkwardly when `{kind}` is `image` ("a image document") and the article disagreement undermined the PR body's claim that future kinds slot in without copy changes. Drop the article and replace `document` with a more neutral noun (`output` / `resultat` / `产物` / `出力` / etc.) so every kind reads naturally: - `produces {kind} output` (English) - `produit un résultat {kind}` (French) - `生成 {kind} 产物` (Simplified Chinese) - … and 14 more `{kind}` placeholder stays literal in every locale; surrounding vocabulary for skill / prompt / chat preserved per existing file conventions.
87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import {
|
|
act,
|
|
cleanup,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
} from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import type { SkillSummary } from '../../src/types';
|
|
|
|
// Regression coverage for nexu-io/open-design#860 (round 3): the modal's
|
|
// onView fires with the modal-internal view id ('preview'), not the
|
|
// active skill id. The Retry path must close over the selected skill so
|
|
// re-fires hit /api/skills/{skill-id}/example, not /api/skills/preview/example.
|
|
|
|
vi.mock('../../src/providers/registry', () => ({
|
|
fetchSkillExample: vi.fn(),
|
|
}));
|
|
|
|
import { fetchSkillExample } from '../../src/providers/registry';
|
|
import { ExamplesTab } from '../../src/components/ExamplesTab';
|
|
|
|
const mockedFetch = fetchSkillExample as unknown as ReturnType<typeof vi.fn>;
|
|
|
|
const sampleSkill: SkillSummary = {
|
|
id: 'live-dashboard',
|
|
name: 'Live Dashboard',
|
|
description: 'A team dashboard live artifact.',
|
|
triggers: ['dashboard'],
|
|
mode: 'prototype',
|
|
previewType: 'html',
|
|
designSystemRequired: false,
|
|
defaultFor: [],
|
|
upstream: null,
|
|
hasBody: true,
|
|
examplePrompt: 'Build me a Notion-style team dashboard.',
|
|
aggregatesExamples: false,
|
|
};
|
|
|
|
async function flushPromises() {
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
});
|
|
}
|
|
|
|
describe('ExamplesTab preview retry path (#860)', () => {
|
|
beforeEach(() => {
|
|
mockedFetch.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
it('Retry refetches the active skill, not the modal-internal view id', async () => {
|
|
mockedFetch.mockResolvedValue({ error: 'simulated failure' });
|
|
|
|
render(<ExamplesTab skills={[sampleSkill]} onUsePrompt={() => {}} />);
|
|
|
|
// Open the preview modal for the sample skill.
|
|
const openButtons = screen.getAllByText(/open preview/i);
|
|
fireEvent.click(openButtons[0]!);
|
|
|
|
// Initial fetch on mount.
|
|
await flushPromises();
|
|
expect(mockedFetch).toHaveBeenCalledTimes(1);
|
|
expect(mockedFetch).toHaveBeenLastCalledWith('live-dashboard', 'html');
|
|
|
|
// Error UI replaces the loading placeholder.
|
|
expect(screen.getByText("Couldn't load this example.")).toBeTruthy();
|
|
const retry = screen.getByRole('button', { name: /retry/i });
|
|
|
|
// Retry must hit the same skill id, NOT 'preview' (the modal view id).
|
|
fireEvent.click(retry);
|
|
await flushPromises();
|
|
|
|
expect(mockedFetch).toHaveBeenCalledTimes(2);
|
|
expect(mockedFetch).toHaveBeenLastCalledWith('live-dashboard', 'html');
|
|
// Defensive: a regression that wires the modal view id back into the
|
|
// fetcher would call with 'preview' as the first arg here, regardless
|
|
// of the previewType arg passed alongside.
|
|
expect(mockedFetch).not.toHaveBeenCalledWith('preview', expect.any(String));
|
|
expect(mockedFetch).not.toHaveBeenCalledWith('preview');
|
|
});
|
|
});
|