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
164 lines
6.1 KiB
TypeScript
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',
|
|
});
|
|
});
|
|
});
|
|
});
|