Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions packages/runtime/src/overlay.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, it, vi } from 'vitest';
import { OVERLAY_SCRIPT } from './overlay';

interface FakeWindow {
addEventListener: (type: string, fn: unknown, capture?: boolean) => void;
parent: { postMessage: (msg: unknown, target: string) => void };
__cs_err?: boolean;
__cs_rej?: boolean;
}

function runOverlay(opts: {
removeThrows?: boolean;
addThrows?: boolean;
}): { warn: ReturnType<typeof vi.fn>; tick: () => void } {
const warn = vi.fn();
const fakeConsole = { warn };

const fakeDocument = {
body: {},
addEventListener: () => {
if (opts.addThrows) throw new Error('add failed');
},
removeEventListener: () => {
if (opts.removeThrows) throw new Error('remove failed');
},
};

const fakeWindow: FakeWindow = {
addEventListener: () => {},
parent: { postMessage: () => {} },
};

let intervalFn: (() => void) | null = null;
const fakeSetInterval = (fn: () => void) => {
intervalFn = fn;
return 1;
};

const sandbox = new Function(
'window',
'document',
'console',
'setInterval',
`with (window) { ${OVERLAY_SCRIPT} }`,
);
sandbox(fakeWindow, fakeDocument, fakeConsole, fakeSetInterval);

return {
warn,
tick: () => {
if (intervalFn) intervalFn();
},
};
}

describe('OVERLAY_SCRIPT reattach loop warning throttle', () => {
it('dedupes repeated reattach failures across many ticks', () => {
const { warn, tick } = runOverlay({ removeThrows: true, addThrows: true });
// Initial reattach already ran inside script; simulate 25 more interval fires (~5s @ 200ms).
for (let i = 0; i < 25; i++) tick();

// 3 install specs * 2 ops (remove+add) = 6 distinct keys at most.
// The point: it must not scale with tick count.
expect(warn.mock.calls.length).toBeLessThanOrEqual(6);
});

it('emits at most one warn per unique error key over the whole loop', () => {
const { warn, tick } = runOverlay({ removeThrows: true });
for (let i = 0; i < 25; i++) tick();
const keys = new Set(warn.mock.calls.map((c) => String(c[0])));
// each warn call should be a unique key
expect(warn.mock.calls.length).toBe(keys.size);
// should be ≤ 3 (one per event type), well under the 25-tick spam ceiling
expect(warn.mock.calls.length).toBeLessThanOrEqual(3);
});
});
22 changes: 14 additions & 8 deletions packages/runtime/src/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
export const OVERLAY_SCRIPT = `(function() {
'use strict';
var hovered = null;
var warned = Object.create(null);
function warnOnce(key, err) {
if (warned[key]) return;
warned[key] = true;
try { console.warn('[overlay] ' + key, err); } catch (_) { /* noop */ }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] warnOnce currently swallows console.warn errors (catch (_) { /* noop */ }), reintroducing a silent fallback. Please let this throw (or surface via existing iframe error channel) so failures are observable with context.

Suggested fix:

function warnOnce(key, err) {
  if (warned[key]) return;
  warned[key] = true;
  console.warn('[overlay] ' + key, err);
}

}

function getXPath(el) {
if (el.dataset && el.dataset.codesignId) return '[data-codesign-id="' + el.dataset.codesignId + '"]';
Expand Down Expand Up @@ -57,7 +63,7 @@ export const OVERLAY_SCRIPT = `(function() {
outerHTML: (el.outerHTML || '').slice(0, 800),
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height }
}, '*');
} catch (_) {}
} catch (err) { console.warn('[overlay] postMessage ELEMENT_SELECTED failed:', err); }
}
function onError(ev) {
try {
Expand All @@ -72,7 +78,7 @@ export const OVERLAY_SCRIPT = `(function() {
stack: ev && ev.error && ev.error.stack ? String(ev.error.stack) : undefined,
timestamp: Date.now()
}, '*');
} catch (_) {}
} catch (err) { console.warn('[overlay] postMessage IFRAME_ERROR (error) failed:', err); }
}
function onRejection(ev) {
try {
Expand All @@ -86,7 +92,7 @@ export const OVERLAY_SCRIPT = `(function() {
stack: (reason && reason.stack) ? String(reason.stack) : undefined,
timestamp: Date.now()
}, '*');
} catch (_) {}
} catch (err) { console.warn('[overlay] postMessage IFRAME_ERROR (unhandledrejection) failed:', err); }
}

// Install + reinstall every 200ms. User code may call removeEventListener
Expand All @@ -100,18 +106,18 @@ export const OVERLAY_SCRIPT = `(function() {
function reattach() {
for (var i = 0; i < installs.length; i++) {
var spec = installs[i];
try { document.removeEventListener(spec.evt, spec.fn, true); } catch (_) {}
try { document.addEventListener(spec.evt, spec.fn, true); } catch (_) {}
try { document.removeEventListener(spec.evt, spec.fn, true); } catch (err) { warnOnce('removeEventListener failed for ' + spec.evt, err); }
try { document.addEventListener(spec.evt, spec.fn, true); } catch (err) { warnOnce('addEventListener failed for ' + spec.evt, err); }
}
if (!window.__cs_err) {
try { window.addEventListener('error', onError, true); window.__cs_err = true; } catch (_) {}
try { window.addEventListener('error', onError, true); window.__cs_err = true; } catch (err) { warnOnce('attach window error listener failed', err); }
}
if (!window.__cs_rej) {
try { window.addEventListener('unhandledrejection', onRejection, true); window.__cs_rej = true; } catch (_) {}
try { window.addEventListener('unhandledrejection', onRejection, true); window.__cs_rej = true; } catch (err) { warnOnce('attach unhandledrejection listener failed', err); }
}
}
reattach();
try { setInterval(reattach, 200); } catch (_) {}
try { setInterval(reattach, 200); } catch (err) { console.warn('[overlay] setInterval reattach failed:', err); }
})();`;

export interface OverlayMessage {
Expand Down
Loading