Files
nexu-io--open-design/apps/web/tests/components/HomeView.web-clone-tracking.test.tsx
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

164 lines
6.1 KiB
TypeScript

// @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<string, unknown> | 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<string, unknown> | undefined;
}
vi.mock('../../src/analytics/provider', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../src/analytics/provider')>();
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(
<I18nProvider initial="en">
<HomeView
projects={[]}
onSubmit={() => undefined}
onOpenProject={() => undefined}
onViewAllProjects={() => undefined}
/>
</I18nProvider>,
);
}
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',
});
});
});
});