// @vitest-environment jsdom // // Web-clone example-card analytics (埋点文档 row 116, element=example_prompt). // The Website-clone examples are plain text prompt cards (no embedded HTML / no // Remix — dropped to avoid reproducing copyrighted site markup). Picking one must // still fire a home chat_composer ui_click with chip_id=web-clone so the // site-clone funnel entry is tracked, complementing the created project's // project_kind=web_clone on project_create_result. import { afterEach, describe, expect, it, vi } from 'vitest'; import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { HomeView } from '../../src/components/HomeView'; import { I18nProvider } from '../../src/i18n'; import { writeHomeGuideStage } from '../../src/components/home-hero/firstRunGuide'; const analyticsMocks = vi.hoisted(() => ({ track: vi.fn() })); // `send()` forwards a trailing request-id arg, so match on (event, props) and // ignore any extra positional args rather than asserting exact arity. function lastClickProps(element: string): Record | undefined { const call = [...analyticsMocks.track.mock.calls] .reverse() .find((args) => args[0] === 'ui_click' && (args[1] as { element?: string })?.element === element); return call?.[1] as Record | undefined; } vi.mock('../../src/analytics/provider', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useAnalytics: () => ({ track: analyticsMocks.track, newRequestId: () => 'request-1', setConfigureGlobals: vi.fn(), setConsent: vi.fn(), setIdentity: vi.fn(), }), }; }); // The Website-clone chip's base scenario (its action.pluginId). It only needs to // exist so clicking the chip binds; the site examples themselves are static text // prompt cards from HOME_PROMPT_EXAMPLES, not plugins. const WEB_CLONE_BASE = { id: 'example-web-clone', title: 'Website clone', version: '0.1.0', trust: 'bundled' as const, sourceKind: 'bundled' as const, source: '/tmp/web-clone', capabilitiesGranted: ['prompt:inject'], fsPath: '/tmp/web-clone', installedAt: 0, updatedAt: 0, manifest: { name: 'example-web-clone', title: 'Website clone', version: '0.1.0', description: 'Recreate an existing website.', tags: ['web-clone', 'website-clone'], od: { kind: 'scenario', taskKind: 'new-generation', useCase: { query: 'Recreate the website at {{targetUrl}}.' }, }, }, }; function stubPlugins() { vi.stubGlobal('fetch', vi.fn(async (url: RequestInfo | URL) => { const href = typeof url === 'string' ? url : url.toString(); if (href === '/api/plugins') { return new Response(JSON.stringify({ plugins: [WEB_CLONE_BASE] }), { status: 200, headers: { 'content-type': 'application/json' }, }); } return new Response(JSON.stringify({}), { status: 200, headers: { 'content-type': 'application/json' }, }); })); } function renderHome() { return render( undefined} onOpenProject={() => undefined} onViewAllProjects={() => undefined} /> , ); } afterEach(() => { vi.unstubAllGlobals(); analyticsMocks.track.mockClear(); cleanup(); window.localStorage.clear(); }); describe('web-clone example-card tracking', () => { it('renders the Website-clone examples as text prompt cards (no plugin preview / no remix)', async () => { writeHomeGuideStage('done'); stubPlugins(); renderHome(); fireEvent.click(await screen.findByTestId('home-hero-rail-web-clone')); // Text prompt cards (site variant: logo tile + bare domain), not plugin cards. const textCards = await screen.findAllByTestId('home-hero-prompt-example'); expect(textCards.length).toBeGreaterThan(0); expect(screen.queryByTestId('home-hero-plugin-presets')).toBeNull(); // No remix/duplicate affordance on the web-clone rail. expect(document.querySelector('[data-testid^="home-hero-plugin-preset-duplicate"]')).toBeNull(); // Site cards show the bare domain (e.g. open-design.ai), not the raw prompt line. expect(textCards.some((c) => (c.textContent ?? '').includes('open-design.ai'))).toBe(true); expect(textCards.every((c) => !(c.textContent ?? '').includes('https://'))).toBe(true); expect(document.querySelector('.home-hero__prompt-example--site')).not.toBeNull(); }); // Contract lock: the shipped Website-clone example set is intentionally // narrowed to the first-party Open Design site to avoid shipping third-party // brand copies. Assert the exact count + domain so the rail can't silently // drift back to the old multi-site set without updating this contract. it('resolves exactly the contracted Open Design Website-clone site card', async () => { writeHomeGuideStage('done'); stubPlugins(); renderHome(); fireEvent.click(await screen.findByTestId('home-hero-rail-web-clone')); const siteCards = await screen.findAllByTestId('home-hero-prompt-example'); const domains = siteCards.map((c) => (c.textContent ?? '').trim()); expect(domains).toEqual(['open-design.ai']); // Every card must be the site variant (favicon tile + bare domain). expect( siteCards.every((c) => c.classList.contains('home-hero__prompt-example--site')), ).toBe(true); }); it('fires element=example_prompt with chip_id=web-clone when a text example is picked', async () => { writeHomeGuideStage('done'); stubPlugins(); renderHome(); fireEvent.click(await screen.findByTestId('home-hero-rail-web-clone')); const textCards = await screen.findAllByTestId('home-hero-prompt-example'); analyticsMocks.track.mockClear(); // ignore the chip-pick ui_click; assert the card event fireEvent.click(textCards[0]!); await waitFor(() => { expect(lastClickProps('example_prompt')).toMatchObject({ page_name: 'home', area: 'chat_composer', element: 'example_prompt', chip_id: 'web-clone', }); }); }); });