mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* feat(web): measure a collapsed deck stage in the preview bridge OPEND-2147 reports deck slides rendering at inconsistent scale, with content cut off. The one confirmed sighting has resisted reproduction across seven eliminations -- the artifact, the host injections, the layout chase, mount timing, in-place rewrites, a hidden tab, and an older build all render the correct scale -- which points at a race rather than a deterministic defect. A race that cannot be reproduced has to be caught in the wild, and today it leaves no trace: the slide is still in the DOM and still paints, so the white-screen probe stays quiet and nothing is recorded. Add a deck-specific probe to the preview observability bridge. When a deck's stage sits at effectively no scale in a settled, visible, sized frame, it reports the measurement -- resolved scale, stage rect, authored canvas size, frame size, document state and time since the bridge started. The host files it as its own event rather than letting it fall through to the runtime-error path, and mirrors one flat line to console at warn level so it reaches renderer.log and therefore od diagnostics export. * test(web): execute the deck stage probe instead of grepping its source Asserting the bridge source contains a string proves nothing about whether the probe can run. The gate it sits behind starts closed and only opens when FileViewer posts the host-state message, so a probe wired to the wrong lifecycle would be dead code that every string-level test still passes. Run the real bridge in a document and assert what it posts: silent until the host reports an active preview, silent for a deck that fitted, silent for a preview with no fixed canvas, and distinguishing a stage never fitted from one fitted to nothing. Mutating the collapse check to never report turns two of these red. * fix(web): measure the deck shapes this product actually ships The probe keyed on .stage plus a --canvas-w custom property. That is one template shape, not the fit contract first-party decks use, so on the decks OPEND-2147 is about it returned at the selector lookup and never sent anything -- telemetry that is silent exactly where it was added to help. Resolve the scaled canvas from the whole family instead: <deck-stage> keeps its canvas in shadow DOM where document.querySelector cannot reach it, the canonical skeleton in prompts/deck-framework.ts authors #deck-stage / .deck-stage at a fixed size and scales it inline, and some design templates use .stage with --canvas-w. This is the same family the export path already targets through DECK_STAGE_SELECTOR. Size now comes from the computed width so all three resolve through one path, and the report carries stage_kind. --canvas-w is no longer a gate. What keeps a 100vw/100vh wrapper quiet is that its canvas tracks the viewport, so it has no fixed canvas to fit. Replaces the string-contains spec with a fixture matrix over the real shapes: collapsed and healthy .deck-stage, collapsed shadow canvas, template .stage, never-fitted, viewport wrapper, and host-gating. Deleting the .deck-stage branch turns three of them red. The suite also now unregisters each bridge it installs -- without that a bridge from an earlier case reports against the next case's DOM. * fix(web): stop a healthy sample from blinding the deck probe, and keep stage_kind Two gaps that both cost triage value on the failure this PR exists to catch. A healthy first sample latched the probe off for the life of the document, so a stage that fitted and then collapsed never reported. OPEND-2147 is a race -- long generation, repeated rewrites, a backgrounded tab -- so late collapse in the same document is the case, not an edge of it. resize and visibilitychange already re-schedule the check; only the latch stood in the way. The white-screen probe alongside it draws the same distinction: it returns on a healthy sample and latches only once it has actually reported. Now this one does too. stage_kind reached the host and died there. The bridge resolves which authored shape collapsed -- canonical .deck-stage, <deck-stage> shadow canvas, template .stage -- and neither the analytics event nor the warn line carried it, so an exported log would show the scale and canvas size without saying which shape produced them. Forwarded to both sinks. Specs first, both red before the fix: a stage healthy at the first tick then collapsed reports on the next trigger, and the host cases assert stage_kind on the event and in the greppable line. Re-latching on healthy, or dropping the field at the host, turns them back red. * fix(web): require a declared canvas before treating .stage as a deck Two things that would have made this instrumentation less trustworthy than no instrumentation. .stage is not a deck marker. Plain pages use that class too -- design-templates/social-carousel is a max-width: 1280px article shell with no fit transform and no authored canvas -- and the probe read its computed width as a 1280px canvas, saw transform: none, and filed a healthy render as a collapsed deck. False positives on that path would pollute both the frequency signal and the exported log, which is worse than being silent. Only the shapes that are markers -- <deck-stage> shadow canvas, #deck-stage, .deck-stage -- may take their computed width as the authored canvas. The generic .stage fallback now has to declare one through --canvas-w / --canvas-h, which is what the templates using that path already do. Also restores apps/web/next-env.d.ts. A local E2E run had Next rewrite its route-types import to a path inside .tmp/e2e/, and git add -A swept it in. That target does not exist in a clean checkout, and the file is generator-owned. Red spec first: a non-deck .stage at 1280px with no custom properties must stay quiet. Restoring the computed-width fallback turns it back red. * fix(web): make the viewport exemption two-dimensional The exemption exists for wrappers that track the viewport, and a wrapper tracks it in both dimensions. Comparing width alone skipped a real failure: a 1920x1080 canvas in a 1920x530 frame still has to scale, because height constrains it to roughly 0.46. The width match returned before resolvedScale() ever ran, so an unfitted deck in a short wide frame produced neither the analytics event nor the exported warning -- the clipped-slide shape OPEND-2147 describes. Now a canvas is exempt only when both dimensions match the frame and the height is usable; anything else goes on to have its transform inspected. Red spec first: canonical 1920x1080 .deck-stage, 1920x530 frame, transform none, must report. Reverting to the width-only comparison turns it back red.
138 lines
5.0 KiB
TypeScript
138 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
PREVIEW_OBSERVABILITY_BRIDGE_MARKER,
|
|
PREVIEW_OBSERVABILITY_MESSAGE_TYPE,
|
|
PREVIEW_OBSERVABILITY_PROTOCOL_VERSION,
|
|
buildPreviewObservabilityBridge,
|
|
parsePreviewObservabilityMessage,
|
|
} from '../../src/runtime/preview-observability.js';
|
|
|
|
describe('preview observability contract', () => {
|
|
it('builds one bounded bridge for runtime, resource, console, and white-screen failures', () => {
|
|
const bridge = buildPreviewObservabilityBridge();
|
|
|
|
expect(bridge).toContain(PREVIEW_OBSERVABILITY_BRIDGE_MARKER);
|
|
expect(bridge).toContain(PREVIEW_OBSERVABILITY_MESSAGE_TYPE);
|
|
expect(bridge).toContain("send('runtime_error'");
|
|
expect(bridge).toContain("send('unhandled_rejection'");
|
|
expect(bridge).toContain("send('console_error'");
|
|
expect(bridge).toContain("send('resource_error'");
|
|
expect(bridge).toContain("send('white_screen'");
|
|
expect(bridge).toContain('stack: text(value.stack, 2000)');
|
|
expect(bridge).toContain('detail.source_url = text(event && event.filename, 1000)');
|
|
expect(bridge).toContain('var MAX_EVENTS = 12');
|
|
expect(bridge).not.toContain('JSON.stringify(arguments)');
|
|
});
|
|
|
|
// OPEND-2147: a deck whose stage collapses to scale ~0 renders as an empty
|
|
// frame while every existing signal stays clean -- the artifact loaded, no
|
|
// script threw, and `visiblePaintCount()` still sees painted chrome, so
|
|
// `white_screen` never fires.
|
|
//
|
|
// This only checks that the bridge carries the probe and its fields at all.
|
|
// Whether the probe fires on the shapes decks are actually authored in --
|
|
// `<deck-stage>` shadow canvas, `.deck-stage`, template `.stage` -- is
|
|
// behavioural and is covered by executing the bridge in
|
|
// apps/web/tests/observability/preview-deck-stage-probe.test.ts. A
|
|
// string-contains assertion cannot tell those apart.
|
|
it('carries the deck stage probe and its measurement fields', () => {
|
|
const bridge = buildPreviewObservabilityBridge();
|
|
expect(bridge).toContain('deck_stage_unscaled');
|
|
for (const field of ['stage_scale_permille', 'stage_transform', 'stage_kind', 'canvas_width', 'elapsed_ms']) {
|
|
expect(bridge).toContain(field);
|
|
}
|
|
// The selector family the export path already targets, so the two cannot
|
|
// drift apart silently.
|
|
for (const selector of ['deck-stage', '#deck-stage, .deck-stage', '.canvas']) {
|
|
expect(bridge).toContain(selector);
|
|
}
|
|
});
|
|
|
|
it('accepts only the versioned preview observability wire shape', () => {
|
|
expect(parsePreviewObservabilityMessage({
|
|
type: PREVIEW_OBSERVABILITY_MESSAGE_TYPE,
|
|
version: 1,
|
|
event: 'runtime_error',
|
|
message: 'boom',
|
|
})).toMatchObject({ event: 'runtime_error', message: 'boom' });
|
|
|
|
expect(parsePreviewObservabilityMessage({
|
|
type: PREVIEW_OBSERVABILITY_MESSAGE_TYPE,
|
|
version: 2,
|
|
event: 'runtime_error',
|
|
})).toBeNull();
|
|
expect(parsePreviewObservabilityMessage({
|
|
type: PREVIEW_OBSERVABILITY_MESSAGE_TYPE,
|
|
version: 1,
|
|
event: 'arbitrary_event',
|
|
})).toBeNull();
|
|
});
|
|
|
|
it('normalizes untrusted fields before returning a bounded payload', () => {
|
|
const parsed = parsePreviewObservabilityMessage({
|
|
type: PREVIEW_OBSERVABILITY_MESSAGE_TYPE,
|
|
version: 1,
|
|
event: 'runtime_error',
|
|
message: ` ${'x'.repeat(600)} `,
|
|
stack: 'line one\nline two',
|
|
line: 12.6,
|
|
viewport_width: 20_000_000,
|
|
blank_observation_count: 2,
|
|
sample_interval_ms: 1_500,
|
|
ignored: 'not part of the protocol',
|
|
});
|
|
|
|
expect(parsed).toMatchObject({
|
|
event: 'runtime_error',
|
|
message: 'x'.repeat(500),
|
|
stack: 'line one line two',
|
|
line: 13,
|
|
viewport_width: 10_000_000,
|
|
blank_observation_count: 2,
|
|
sample_interval_ms: 1_500,
|
|
});
|
|
expect(parsed).not.toHaveProperty('ignored');
|
|
});
|
|
|
|
it('accepts the deck stage measurement as a versioned event', () => {
|
|
expect(parsePreviewObservabilityMessage({
|
|
type: PREVIEW_OBSERVABILITY_MESSAGE_TYPE,
|
|
version: PREVIEW_OBSERVABILITY_PROTOCOL_VERSION,
|
|
event: 'deck_stage_unscaled',
|
|
stage_scale_permille: 0,
|
|
stage_transform: 'matrix',
|
|
stage_kind: 'deck-stage',
|
|
stage_width: 0,
|
|
stage_height: 0,
|
|
canvas_width: 1920,
|
|
canvas_height: 1080,
|
|
viewport_width: 1075,
|
|
viewport_height: 530,
|
|
elapsed_ms: 5000,
|
|
})).toMatchObject({
|
|
event: 'deck_stage_unscaled',
|
|
stage_kind: 'deck-stage',
|
|
stage_transform: 'matrix',
|
|
stage_width: 0,
|
|
canvas_width: 1920,
|
|
canvas_height: 1080,
|
|
elapsed_ms: 5000,
|
|
});
|
|
});
|
|
|
|
it('rejects known fields with invalid types', () => {
|
|
expect(parsePreviewObservabilityMessage({
|
|
type: PREVIEW_OBSERVABILITY_MESSAGE_TYPE,
|
|
version: 1,
|
|
event: 'runtime_error',
|
|
message: { nested: 'boom' },
|
|
})).toBeNull();
|
|
expect(parsePreviewObservabilityMessage({
|
|
type: PREVIEW_OBSERVABILITY_MESSAGE_TYPE,
|
|
version: 1,
|
|
event: 'runtime_error',
|
|
line: '12',
|
|
})).toBeNull();
|
|
});
|
|
});
|