mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
Use a fake clock to verify the existing 10ms deadline and the final readiness snapshot after multiple inspections. Cover a first inspection that consumes the whole budget, where a single observation is valid. Keep the readiness helper and application behavior unchanged. Co-authored-by: lefarcen <935902669@qq.com>
292 lines
9.5 KiB
TypeScript
292 lines
9.5 KiB
TypeScript
import { runInNewContext } from 'node:vm';
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
PACKAGED_HOME_FIRST_RUN_PROMPT,
|
|
packagedHomeFirstRunExpression,
|
|
waitForPackagedHomeFirstRunSetup,
|
|
} from '@/vitest/packaged-home-first-run';
|
|
|
|
class FixtureElement {
|
|
__lexicalEditor?: {
|
|
parseEditorState(value: string): unknown;
|
|
setEditorState(value: unknown): void;
|
|
};
|
|
|
|
isContentEditable = false;
|
|
textContent = '';
|
|
|
|
constructor(private readonly visible = true) {}
|
|
|
|
focus(): void {}
|
|
|
|
getClientRects(): ArrayLike<unknown> {
|
|
return this.visible ? [{}] : [];
|
|
}
|
|
}
|
|
|
|
type FixtureDocumentOptions = {
|
|
composerAfterQueries?: number;
|
|
composerContentEditable?: boolean;
|
|
composerVisible?: boolean;
|
|
editorAfterQueries?: number;
|
|
loadingVisible?: boolean;
|
|
onboardingVisible?: boolean;
|
|
promptInsertionFailures?: number;
|
|
};
|
|
|
|
function renderFixture(options: FixtureDocumentOptions = {}) {
|
|
const input = new FixtureElement(options.composerVisible ?? true);
|
|
input.isContentEditable = options.composerContentEditable ?? true;
|
|
let promptInsertionFailures = options.promptInsertionFailures ?? 0;
|
|
const editor = {
|
|
parseEditorState: (value: string) => JSON.parse(value),
|
|
setEditorState: (value: unknown) => {
|
|
if (promptInsertionFailures > 0) {
|
|
promptInsertionFailures -= 1;
|
|
throw new Error('fixture prompt insertion failed');
|
|
}
|
|
const root = value as {
|
|
root?: { children?: Array<{ children?: Array<{ text?: string }> }> };
|
|
};
|
|
input.textContent = root.root?.children?.[0]?.children?.[0]?.text ?? '';
|
|
},
|
|
};
|
|
if (options.editorAfterQueries == null) input.__lexicalEditor = editor;
|
|
|
|
const loading = options.loadingVisible ? new FixtureElement() : null;
|
|
const onboarding = options.onboardingVisible ? new FixtureElement() : null;
|
|
let composerQueries = 0;
|
|
|
|
return {
|
|
document: {
|
|
addEventListener: () => undefined,
|
|
querySelector: (selector: string) => {
|
|
if (selector === '[data-testid="home-hero-input"]') {
|
|
composerQueries += 1;
|
|
if (composerQueries > (options.editorAfterQueries ?? Number.POSITIVE_INFINITY)) {
|
|
input.__lexicalEditor = editor;
|
|
}
|
|
return composerQueries > (options.composerAfterQueries ?? 0) ? input : null;
|
|
}
|
|
if (selector === '.od-loading-shell, .centered-loader') return loading;
|
|
if (selector === '.entry-shell--onboarding, .entry-onboarding-modal') return onboarding;
|
|
return null;
|
|
},
|
|
},
|
|
input,
|
|
composerQueries: () => composerQueries,
|
|
};
|
|
}
|
|
|
|
function createExpressionEvaluator(
|
|
fixture: ReturnType<typeof renderFixture>,
|
|
pathname = '/',
|
|
) {
|
|
const sandbox = {
|
|
document: fixture.document,
|
|
Element: FixtureElement,
|
|
HTMLElement: FixtureElement,
|
|
Headers,
|
|
location: { href: `od://app${pathname}`, pathname },
|
|
performance: {
|
|
getEntriesByType: () => [{}],
|
|
timeOrigin: 1234,
|
|
},
|
|
Request,
|
|
Response,
|
|
fetch: async () => new Response('{}', { status: 200 }),
|
|
setTimeout,
|
|
};
|
|
return async (expression: string): Promise<unknown> => {
|
|
return await runInNewContext(expression, sandbox);
|
|
};
|
|
}
|
|
|
|
async function evaluateExpression(
|
|
expression: string,
|
|
fixture: ReturnType<typeof renderFixture>,
|
|
pathname = '/',
|
|
): Promise<unknown> {
|
|
return await createExpressionEvaluator(fixture, pathname)(expression);
|
|
}
|
|
|
|
describe('packaged Home first-run readiness', () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('returns quickly until a later inspection can instrument the ready composer', async () => {
|
|
const fixture = renderFixture({ composerAfterQueries: 2, loadingVisible: true });
|
|
|
|
const first = await evaluateExpression(packagedHomeFirstRunExpression(), fixture);
|
|
const second = await evaluateExpression(packagedHomeFirstRunExpression(), fixture);
|
|
const ready = await evaluateExpression(packagedHomeFirstRunExpression(), fixture);
|
|
|
|
expect(first).toMatchObject({
|
|
instrumented: false,
|
|
readiness: { composerFound: false, composerVisible: false },
|
|
});
|
|
expect(second).toMatchObject({
|
|
instrumented: false,
|
|
readiness: { composerFound: false, composerVisible: false },
|
|
});
|
|
expect(fixture.composerQueries()).toBe(4);
|
|
expect(fixture.input.textContent).toBe(PACKAGED_HOME_FIRST_RUN_PROMPT);
|
|
expect(ready).toMatchObject({
|
|
instrumented: true,
|
|
inputTextBeforeSubmit: PACKAGED_HOME_FIRST_RUN_PROMPT,
|
|
submitClicked: false,
|
|
});
|
|
});
|
|
|
|
it('polls through separate inspections until the composer can be instrumented', async () => {
|
|
const fixture = renderFixture({ composerAfterQueries: 2, loadingVisible: true });
|
|
let inspectionCount = 0;
|
|
|
|
const setup = await waitForPackagedHomeFirstRunSetup(async () => {
|
|
inspectionCount += 1;
|
|
return await evaluateExpression(packagedHomeFirstRunExpression(), fixture);
|
|
}, { pollIntervalMs: 0, timeoutMs: 100 });
|
|
|
|
expect(inspectionCount).toBe(3);
|
|
expect(setup).toMatchObject({
|
|
instrumented: true,
|
|
inputTextBeforeSubmit: PACKAGED_HOME_FIRST_RUN_PROMPT,
|
|
});
|
|
});
|
|
|
|
it('waits for Lexical attachment without persisting a poisoned setup', async () => {
|
|
const fixture = renderFixture({ editorAfterQueries: 2 });
|
|
const evaluate = createExpressionEvaluator(fixture);
|
|
|
|
const first = await evaluate(packagedHomeFirstRunExpression());
|
|
const second = await evaluate(packagedHomeFirstRunExpression());
|
|
const ready = await evaluate(packagedHomeFirstRunExpression());
|
|
|
|
expect(first).toMatchObject({
|
|
instrumented: false,
|
|
readiness: { lexicalEditorReady: false },
|
|
});
|
|
expect(second).toMatchObject({
|
|
instrumented: false,
|
|
readiness: { lexicalEditorReady: false },
|
|
});
|
|
expect(ready).toMatchObject({
|
|
instrumented: true,
|
|
inputTextBeforeSubmit: PACKAGED_HOME_FIRST_RUN_PROMPT,
|
|
readiness: { lexicalEditorReady: true },
|
|
});
|
|
});
|
|
|
|
it('retries prompt insertion without accepting provisional success state', async () => {
|
|
const fixture = renderFixture({ promptInsertionFailures: 1 });
|
|
const evaluate = createExpressionEvaluator(fixture);
|
|
let inspectionCount = 0;
|
|
|
|
const setup = await waitForPackagedHomeFirstRunSetup(async () => {
|
|
inspectionCount += 1;
|
|
return await evaluate(packagedHomeFirstRunExpression());
|
|
}, { pollIntervalMs: 0, timeoutMs: 100 });
|
|
|
|
expect(inspectionCount).toBe(2);
|
|
expect(fixture.input.textContent).toBe(PACKAGED_HOME_FIRST_RUN_PROMPT);
|
|
expect(setup.inputTextBeforeSubmit).toBe(PACKAGED_HOME_FIRST_RUN_PROMPT);
|
|
});
|
|
|
|
it('reports why the current composer is not ready without blocking the inspection', async () => {
|
|
const fixture = renderFixture({
|
|
composerContentEditable: false,
|
|
composerVisible: false,
|
|
loadingVisible: true,
|
|
onboardingVisible: true,
|
|
});
|
|
const value = await evaluateExpression(
|
|
packagedHomeFirstRunExpression(),
|
|
fixture,
|
|
'/onboarding',
|
|
);
|
|
|
|
expect(value).toMatchObject({
|
|
instrumented: false,
|
|
readiness: {
|
|
pathname: '/onboarding',
|
|
loadingVisible: true,
|
|
onboardingVisible: true,
|
|
composerFound: true,
|
|
composerVisible: false,
|
|
composerContentEditable: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('times out with the final readiness snapshot when no inspection can instrument', async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(0);
|
|
const initialFixture = renderFixture({ composerAfterQueries: 1, loadingVisible: true });
|
|
const finalFixture = renderFixture({
|
|
composerContentEditable: false,
|
|
composerVisible: false,
|
|
loadingVisible: true,
|
|
onboardingVisible: true,
|
|
});
|
|
let inspectionCount = 0;
|
|
let settled = false;
|
|
|
|
const setup = waitForPackagedHomeFirstRunSetup(async () => {
|
|
inspectionCount += 1;
|
|
return await evaluateExpression(
|
|
packagedHomeFirstRunExpression(),
|
|
inspectionCount === 1 ? initialFixture : finalFixture,
|
|
inspectionCount === 1 ? '/' : '/onboarding',
|
|
);
|
|
}, { pollIntervalMs: 1, timeoutMs: 10 });
|
|
// Attach the rejection handler before advancing the deadline.
|
|
const failure = setup.catch((error: unknown) => {
|
|
settled = true;
|
|
return error;
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(9);
|
|
expect(settled).toBe(false);
|
|
expect(inspectionCount).toBe(10);
|
|
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
await expect(failure).resolves.toMatchObject({
|
|
message: expect.stringMatching(
|
|
/pathname.*\/onboarding.*loadingVisible.*true.*onboardingVisible.*true.*composerFound.*true.*composerVisible.*false.*composerContentEditable.*false/s,
|
|
),
|
|
});
|
|
expect(inspectionCount).toBe(10);
|
|
});
|
|
|
|
it('reports the sole snapshot when the first inspection exhausts the budget', async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(0);
|
|
const fixture = renderFixture({
|
|
composerContentEditable: false,
|
|
composerVisible: false,
|
|
loadingVisible: true,
|
|
onboardingVisible: true,
|
|
});
|
|
let inspectionCount = 0;
|
|
|
|
const setup = waitForPackagedHomeFirstRunSetup(async () => {
|
|
inspectionCount += 1;
|
|
// A slow inspection can consume the full budget before a retry is due.
|
|
vi.setSystemTime(10);
|
|
return await evaluateExpression(
|
|
packagedHomeFirstRunExpression(),
|
|
fixture,
|
|
'/onboarding',
|
|
);
|
|
}, { pollIntervalMs: 1, timeoutMs: 10 });
|
|
|
|
await expect(setup).rejects.toThrow(
|
|
/pathname.*\/onboarding.*loadingVisible.*true.*onboardingVisible.*true.*composerFound.*true.*composerVisible.*false.*composerContentEditable.*false/s,
|
|
);
|
|
expect(inspectionCount).toBe(1);
|
|
});
|
|
});
|