mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 20:27:22 +00:00
feat: add developer-mode startup and stack hydration timing (#1619)
* feat: add developer-mode startup and stack hydration timing Instrument boot-to-list and detail hydration with commit-aligned milestones, truthful request stages, and destination/gateway debug duration logs so performance work is guided by measurements. * fix: redact stack names and complete hydration request stages Stop logging stack identifiers in containers debug timing, and record state_dispatch (plus detail fetch spans) so copied reports match the advertised stage breakdown.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { HydrationReport, HydrationSnapshot } from '@/lib/hydrationTiming';
|
||||
|
||||
let mockSnapshot: HydrationSnapshot;
|
||||
let mockListVisibleMs: number | null;
|
||||
let mockReport: HydrationReport;
|
||||
|
||||
vi.mock('@/hooks/useHydrationTiming', () => ({
|
||||
useHydrationTiming: () => ({ snapshot: mockSnapshot, listVisibleMs: mockListVisibleMs }),
|
||||
}));
|
||||
|
||||
const clearReportMock = vi.fn();
|
||||
vi.mock('@/lib/hydrationTiming', () => ({
|
||||
getHydrationReport: () => mockReport,
|
||||
clearReport: () => clearReportMock(),
|
||||
}));
|
||||
|
||||
const copyMock = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mock('@/lib/clipboard', () => ({ copyToClipboard: (text: string) => copyMock(text) }));
|
||||
|
||||
import { HydrationTimingPanel } from '../HydrationTimingPanel';
|
||||
|
||||
function snapshot(events: HydrationSnapshot['events'] = []): HydrationSnapshot {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
clock: 'performance.now',
|
||||
bootSessionId: 'boot-1',
|
||||
bootStartAt: 0,
|
||||
nodeSessionId: 'node-2',
|
||||
nodeId: 1,
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
function report(over: Partial<HydrationReport> = {}): HydrationReport {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
capturedAt: 0,
|
||||
clock: 'performance.now',
|
||||
bootSessionId: 'boot-1',
|
||||
nodeSessionId: 'node-2',
|
||||
nodeId: 1,
|
||||
listVisibleMs: 1200,
|
||||
anyProxied: false,
|
||||
phases: [
|
||||
{ phase: 'boot_start', kind: 'milestone', offsetMs: 0, critical: true, outcome: 'ok' },
|
||||
{ phase: 'list_visible', kind: 'milestone', offsetMs: 1200, uiCommitMs: 1200, critical: true, outcome: 'ok' },
|
||||
],
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearReportMock.mockClear();
|
||||
copyMock.mockClear();
|
||||
mockSnapshot = snapshot();
|
||||
mockListVisibleMs = 1200;
|
||||
mockReport = report();
|
||||
});
|
||||
|
||||
describe('HydrationTimingPanel', () => {
|
||||
it('shows the list_visible elapsed time on the collapsed chip', () => {
|
||||
render(<HydrationTimingPanel />);
|
||||
expect(screen.getByTestId('hydration-chip')).toHaveTextContent('list 1.2s');
|
||||
});
|
||||
|
||||
it('shows an ellipsis before list_visible commits', () => {
|
||||
mockListVisibleMs = null;
|
||||
render(<HydrationTimingPanel />);
|
||||
expect(screen.getByTestId('hydration-chip')).toHaveTextContent('list …');
|
||||
});
|
||||
|
||||
it('expands into the phase table and collapses again', () => {
|
||||
render(<HydrationTimingPanel />);
|
||||
fireEvent.click(screen.getByTestId('hydration-chip'));
|
||||
|
||||
expect(screen.getByTestId('hydration-panel')).toBeInTheDocument();
|
||||
expect(screen.getByText('list_visible')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('hydration-collapse'));
|
||||
expect(screen.queryByTestId('hydration-panel')).toBeNull();
|
||||
expect(screen.getByTestId('hydration-chip')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('collapses on Escape', () => {
|
||||
render(<HydrationTimingPanel />);
|
||||
fireEvent.click(screen.getByTestId('hydration-chip'));
|
||||
act(() => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||
});
|
||||
expect(screen.queryByTestId('hydration-panel')).toBeNull();
|
||||
});
|
||||
|
||||
it('copies the hydration report as pretty JSON', async () => {
|
||||
render(<HydrationTimingPanel />);
|
||||
fireEvent.click(screen.getByTestId('hydration-chip'));
|
||||
fireEvent.click(screen.getByTestId('hydration-copy'));
|
||||
|
||||
await waitFor(() => expect(copyMock).toHaveBeenCalledTimes(1));
|
||||
expect(copyMock).toHaveBeenCalledWith(JSON.stringify(mockReport, null, 2));
|
||||
});
|
||||
|
||||
it('clears the report', () => {
|
||||
render(<HydrationTimingPanel />);
|
||||
fireEvent.click(screen.getByTestId('hydration-chip'));
|
||||
fireEvent.click(screen.getByTestId('hydration-clear'));
|
||||
expect(clearReportMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('notes gateway developer mode when any event was proxied', () => {
|
||||
mockReport = report({ anyProxied: true });
|
||||
render(<HydrationTimingPanel />);
|
||||
fireEvent.click(screen.getByTestId('hydration-chip'));
|
||||
expect(screen.getByText(/gateway/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user