项目文件夹

文件
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

89 行
3.4 KiB
TypeScript

// @vitest-environment jsdom
//
// Regression test for the recommendation handoff race (PR #5111 review). The
// Home→Studio onboarding entry used to be stashed in a single global
// sessionStorage slot BEFORE the project was created, which let an unrelated
// project opened mid-create steal the personalized context. The fix keys the
// handoff by the created project id and moves the stash into the create success
// path — so `RecommendedStartRegion` must NOT touch sessionStorage itself and
// must instead hand the entry to `onStart`, letting the create pipeline stash
// it once the target id is known.
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { I18nProvider } from '../../src/i18n';
import { RecommendedStartRegion } from '../../src/components/RecommendedStartRegion';
import { buildRecommendation } from '../../src/onboarding/recommendation';
afterEach(() => {
cleanup();
window.sessionStorage.clear();
});
type OnStart = (input: {
name: string;
prompt: string;
metadata: unknown;
onboardingEntry: unknown;
}) => unknown;
function renderRegion(onStart: OnStart) {
const recommendation = buildRecommendation({ role: 'designer', useCases: ['prototype'] });
return render(
<I18nProvider initial="en">
<RecommendedStartRegion
recommendation={recommendation}
onStart={onStart as never}
onDismiss={() => undefined}
/>
</I18nProvider>,
);
}
describe('RecommendedStartRegion — Start in Studio handoff', () => {
it('hands the onboarding entry to onStart (create pipeline keys it by project id)', async () => {
let received: Parameters<OnStart>[0] | null = null;
const onStart = vi.fn((input: Parameters<OnStart>[0]) => {
received = input;
return Promise.resolve(true);
});
renderRegion(onStart);
fireEvent.click(screen.getByTestId('home-recommendation-start'));
await waitFor(() => expect(onStart).toHaveBeenCalledTimes(1));
expect(received).toMatchObject({
onboardingEntry: {
source: 'home_recommendation',
recommendationId: 'product_ui_prototype',
},
});
});
it('never writes the handoff to sessionStorage itself', async () => {
const setItem = vi.spyOn(window.sessionStorage, 'setItem');
const onStart = vi.fn(async () => true);
renderRegion(onStart);
fireEvent.click(screen.getByTestId('home-recommendation-start'));
await waitFor(() => expect(onStart).toHaveBeenCalledTimes(1));
await Promise.resolve();
expect(setItem).not.toHaveBeenCalled();
setItem.mockRestore();
});
it('re-enables the CTA for retry when the start fails', async () => {
// `onStart` (the Home wrapper) surfaces its own visible error and resolves
// `false` on failure — it never rejects. The region should drop its pending
// state so the user can retry, and never leave a stale sessionStorage slot.
const onStart = vi.fn(async () => false);
renderRegion(onStart);
const cta = screen.getByTestId('home-recommendation-start') as HTMLButtonElement;
fireEvent.click(cta);
await waitFor(() => expect(onStart).toHaveBeenCalledTimes(1));
await waitFor(() => expect(cta.disabled).toBe(false));
expect(window.sessionStorage.length).toBe(0);
// Retry is possible: a second click fires onStart again.
fireEvent.click(cta);
await waitFor(() => expect(onStart).toHaveBeenCalledTimes(2));
});
});