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
114 lines
3.0 KiB
TypeScript
114 lines
3.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// Regression for the hint-impression inflation bug (PR #5111 review). The card
|
|
// returns `null` until the 600ms layout-settle window elapses, but the
|
|
// `surface_view` impression used to fire on mount regardless of `settled`. A
|
|
// mount removed during settle would then record an impression the user never
|
|
// saw, inflating the `onboarding_first_artifact_hint` denominator. The fix
|
|
// gates the impression on `settled`.
|
|
|
|
import { act, cleanup, render } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
surfaceView: vi.fn(),
|
|
clickFn: vi.fn(),
|
|
seen: { value: false },
|
|
}));
|
|
|
|
vi.mock('../../src/analytics/events', () => ({
|
|
trackStudioOnboardingHintSurfaceView: mocks.surfaceView,
|
|
trackStudioOnboardingHintClick: mocks.clickFn,
|
|
}));
|
|
|
|
vi.mock('../../src/onboarding/first-artifact-hint', () => ({
|
|
hasSeenFirstArtifactHint: () => mocks.seen.value,
|
|
markFirstArtifactHintSeen: () => {
|
|
mocks.seen.value = true;
|
|
},
|
|
}));
|
|
|
|
import { I18nProvider } from '../../src/i18n';
|
|
import { FirstArtifactHint } from '../../src/components/FirstArtifactHint';
|
|
|
|
beforeEach(() => {
|
|
mocks.surfaceView.mockClear();
|
|
mocks.clickFn.mockClear();
|
|
mocks.seen.value = false;
|
|
// motion/react's useReducedMotion reads matchMedia; jsdom omits it.
|
|
if (!window.matchMedia) {
|
|
window.matchMedia = ((query: string) => ({
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addListener: () => {},
|
|
removeListener: () => {},
|
|
addEventListener: () => {},
|
|
removeEventListener: () => {},
|
|
dispatchEvent: () => false,
|
|
})) as unknown as typeof window.matchMedia;
|
|
}
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
act(() => {
|
|
vi.runOnlyPendingTimers();
|
|
});
|
|
vi.useRealTimers();
|
|
cleanup();
|
|
});
|
|
|
|
function renderHint() {
|
|
return render(
|
|
<I18nProvider initial="en">
|
|
<FirstArtifactHint />
|
|
</I18nProvider>,
|
|
);
|
|
}
|
|
|
|
describe('FirstArtifactHint — surface_view impression', () => {
|
|
it('does not fire before the settle window elapses', () => {
|
|
act(() => {
|
|
renderHint();
|
|
});
|
|
expect(mocks.surfaceView).not.toHaveBeenCalled();
|
|
act(() => {
|
|
vi.advanceTimersByTime(300);
|
|
});
|
|
expect(mocks.surfaceView).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fires exactly once after the card becomes renderable', () => {
|
|
act(() => {
|
|
renderHint();
|
|
});
|
|
act(() => {
|
|
vi.advanceTimersByTime(600);
|
|
});
|
|
expect(mocks.surfaceView).toHaveBeenCalledTimes(1);
|
|
// Later re-measures / resizes must not re-fire.
|
|
act(() => {
|
|
vi.advanceTimersByTime(3000);
|
|
});
|
|
expect(mocks.surfaceView).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('never fires for a mount removed during the settle window', () => {
|
|
let unmount = () => {};
|
|
act(() => {
|
|
unmount = renderHint().unmount;
|
|
});
|
|
act(() => {
|
|
vi.advanceTimersByTime(300);
|
|
});
|
|
act(() => {
|
|
unmount();
|
|
});
|
|
act(() => {
|
|
vi.advanceTimersByTime(600);
|
|
});
|
|
expect(mocks.surfaceView).not.toHaveBeenCalled();
|
|
});
|
|
});
|