Files
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

127 lines
4.7 KiB
TypeScript

// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
clearExceptionTrackingContext,
setExceptionTrackingContext,
} from '../../src/analytics/error-tracking';
import { installWhiteScreenDetector } from '../../src/observability/white-screen';
/**
* The detector is allowed to be conservative on the false-negative side
* (don't fire when the app actually rendered something) but must fire
* when the user is really stuck on a non-app screen. The critical case —
* called out by codex review on PR #2527 — is the dynamic-import loading
* shell: `<div class="od-loading-shell">Loading Open Design…</div>`. That
* string is well above the visible-text floor, so an earlier
* implementation that only checked `body.innerText.length` would silently
* treat the loading sentinel as a successful mount and cancel the timer.
*/
const fetchMock = vi.fn();
const ORIGINAL_FETCH = globalThis.fetch;
beforeEach(() => {
fetchMock.mockReset();
fetchMock.mockResolvedValue(new Response('', { status: 200 }));
globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
// Pre-arm the transport so the buffered event flushes immediately.
setExceptionTrackingContext({
apiKey: 'phc_test',
host: 'https://us.i.posthog.com',
distinctId: 'white-screen-test',
});
vi.useFakeTimers({ shouldAdvanceTime: false });
document.body.innerHTML = '';
document.documentElement.removeAttribute('data-od-app-mounted');
});
afterEach(() => {
vi.useRealTimers();
clearExceptionTrackingContext();
globalThis.fetch = ORIGINAL_FETCH;
document.body.innerHTML = '';
document.documentElement.removeAttribute('data-od-app-mounted');
});
function lastSentEvent(): { event: string; properties: Record<string, unknown> } | null {
const lastCall = fetchMock.mock.calls.at(-1);
if (!lastCall) return null;
const init = lastCall[1] as RequestInit;
return JSON.parse(init.body as string) as { event: string; properties: Record<string, unknown> };
}
describe('observability/white-screen', () => {
it('fires client_white_screen when only the dynamic-import loading shell is in the DOM after the timeout', () => {
// Reproduces the codex-review reported bug: the loading shell text
// "Loading Open Design…" is longer than the legacy 10-char floor.
const shell = document.createElement('div');
shell.className = 'od-loading-shell';
shell.textContent = 'Loading Open Design…';
document.body.appendChild(shell);
installWhiteScreenDetector();
// Drive the 5s timeout. requestIdleCallback/setTimeout are both fake-
// timer-aware via vi.useFakeTimers above.
vi.advanceTimersByTime(6000);
expect(fetchMock).toHaveBeenCalled();
const sent = lastSentEvent();
expect(sent?.event).toBe('client_white_screen');
expect(sent?.properties).toMatchObject({
reason: 'app_not_mounted_after_timeout',
timeout_ms: 5000,
});
});
it('does NOT fire when the app sets the data-od-app-mounted marker before the timeout', () => {
// Simulate App.tsx's first useEffect setting the attribute.
document.documentElement.setAttribute('data-od-app-mounted', '1');
installWhiteScreenDetector();
vi.advanceTimersByTime(6000);
expect(fetchMock).not.toHaveBeenCalled();
});
it('cancels the timer the moment a non-loading-shell child appears with real content', () => {
// Start with only the loading shell.
const shell = document.createElement('div');
shell.className = 'od-loading-shell';
shell.textContent = 'Loading Open Design…';
document.body.appendChild(shell);
installWhiteScreenDetector();
// Wait halfway through the timeout window — still only loading shell.
vi.advanceTimersByTime(2500);
expect(fetchMock).not.toHaveBeenCalled();
// Now the real App mounts — adds a meaningful child alongside the shell.
const real = document.createElement('div');
real.className = 'workspace-shell';
real.textContent = 'New project · Recent · Plugins · …';
document.body.appendChild(real);
// Let MutationObserver microtasks process. jsdom runs them
// synchronously after the mutation, but we still need to flush the
// scheduled idle/timer queue.
return Promise.resolve().then(() => {
vi.advanceTimersByTime(3500);
expect(fetchMock).not.toHaveBeenCalled();
});
});
it('fires when only sub-MIN_VISIBLE_TEXT non-shell content is present (still effectively blank)', () => {
const tiny = document.createElement('div');
tiny.textContent = '...';
document.body.appendChild(tiny);
installWhiteScreenDetector();
vi.advanceTimersByTime(6000);
expect(fetchMock).toHaveBeenCalled();
const sent = lastSentEvent();
expect(sent?.event).toBe('client_white_screen');
});
});