mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Extract live browser DOM helpers (#239)
This commit is contained in:
@@ -106,6 +106,7 @@ export const SUITES = {
|
||||
files: [
|
||||
'tests/live-accept.test.mjs',
|
||||
'tests/live-accept-scrub.test.mjs',
|
||||
'tests/live-browser-dom.test.mjs',
|
||||
'tests/live-browser-script-parts.test.mjs',
|
||||
'tests/live-browser-regression.test.mjs',
|
||||
'tests/live-browser-session.test.mjs',
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Browser-side DOM helpers for Impeccable live mode.
|
||||
*
|
||||
* Kept separate from live-browser.js so future browser script parts can share
|
||||
* chrome mounting, lookup, focus, and picker helpers without depending on the
|
||||
* full overlay UI bundle.
|
||||
*/
|
||||
(function (root) {
|
||||
'use strict';
|
||||
if (!root) return;
|
||||
|
||||
function createLiveBrowserDomHelpers({
|
||||
prefix,
|
||||
skipTags,
|
||||
document: doc = root.document,
|
||||
css = root.CSS,
|
||||
crypto = root.crypto,
|
||||
} = {}) {
|
||||
if (!prefix) throw new Error('prefix required');
|
||||
if (!doc) throw new Error('document required');
|
||||
const tagsToSkip = skipTags || new Set();
|
||||
|
||||
function own(el) {
|
||||
return el && (el.id?.startsWith(prefix) || el.closest?.('[id^="' + prefix + '"]'));
|
||||
}
|
||||
|
||||
function pickable(el) {
|
||||
if (!el || el.nodeType !== 1) return false;
|
||||
if (tagsToSkip.has(String(el.tagName || '').toLowerCase())) return false;
|
||||
if (own(el)) return false;
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.width >= 20 && r.height >= 20;
|
||||
}
|
||||
|
||||
function desc(el) {
|
||||
if (!el) return '';
|
||||
let s = el.tagName.toLowerCase();
|
||||
if (el.id) s += '#' + el.id;
|
||||
else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.');
|
||||
return s;
|
||||
}
|
||||
|
||||
function rectIsUsableAnchor(rect) {
|
||||
return !!rect && rect.width > 0.5 && rect.height > 0.5;
|
||||
}
|
||||
|
||||
function makeFrozenAnchor(el) {
|
||||
if (!el || !el.getBoundingClientRect) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (!rectIsUsableAnchor(r)) return null;
|
||||
const rect = {
|
||||
x: r.x, y: r.y,
|
||||
top: r.top, left: r.left,
|
||||
right: r.right, bottom: r.bottom,
|
||||
width: r.width, height: r.height,
|
||||
};
|
||||
return {
|
||||
__impeccableFrozenAnchor: true,
|
||||
tagName: el.tagName || 'DIV',
|
||||
id: el.id || '',
|
||||
classList: el.classList ? [...el.classList] : [],
|
||||
hasAttribute: () => false,
|
||||
getBoundingClientRect: () => rect,
|
||||
};
|
||||
}
|
||||
|
||||
function id8() {
|
||||
if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8);
|
||||
return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8);
|
||||
}
|
||||
|
||||
function cssId(id) {
|
||||
if (css?.escape) return css.escape(id);
|
||||
return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
|
||||
}
|
||||
|
||||
function liveUiRoot() {
|
||||
const uiRoot = root.__IMPECCABLE_LIVE_UI_ROOT__;
|
||||
if (uiRoot && typeof uiRoot.appendChild === 'function') return uiRoot;
|
||||
return doc.body;
|
||||
}
|
||||
|
||||
function uiAppend(el) {
|
||||
liveUiRoot().appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function uiAppendStyle(styleEl) {
|
||||
const uiRoot = liveUiRoot();
|
||||
if (uiRoot && uiRoot !== doc.body) uiRoot.appendChild(styleEl);
|
||||
else doc.head.appendChild(styleEl);
|
||||
return styleEl;
|
||||
}
|
||||
|
||||
function uiGetById(id) {
|
||||
const uiRoot = liveUiRoot();
|
||||
if (uiRoot?.getElementById) {
|
||||
const found = uiRoot.getElementById(id);
|
||||
if (found) return found;
|
||||
}
|
||||
if (uiRoot?.querySelector) {
|
||||
const found = uiRoot.querySelector('#' + cssId(id));
|
||||
if (found) return found;
|
||||
}
|
||||
return doc.getElementById(id);
|
||||
}
|
||||
|
||||
function activeElementDeep() {
|
||||
let active = doc.activeElement;
|
||||
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
||||
return active;
|
||||
}
|
||||
|
||||
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
|
||||
if (!rootEl) return;
|
||||
if (setPointerEvents) {
|
||||
rootEl.style.setProperty('pointer-events', 'auto', 'important');
|
||||
}
|
||||
const stop = (e) => e.stopPropagation();
|
||||
rootEl.addEventListener('pointerdown', stop);
|
||||
rootEl.addEventListener('mousedown', stop);
|
||||
rootEl.addEventListener('focusin', stop);
|
||||
}
|
||||
|
||||
return {
|
||||
own,
|
||||
pickable,
|
||||
desc,
|
||||
rectIsUsableAnchor,
|
||||
makeFrozenAnchor,
|
||||
id8,
|
||||
cssId,
|
||||
liveUiRoot,
|
||||
uiAppend,
|
||||
uiAppendStyle,
|
||||
uiGetById,
|
||||
activeElementDeep,
|
||||
defangOutsideHandlers,
|
||||
};
|
||||
}
|
||||
|
||||
root.__IMPECCABLE_LIVE_DOM__ = {
|
||||
version: 1,
|
||||
createLiveBrowserDomHelpers,
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
+24
-125
@@ -195,93 +195,31 @@
|
||||
// Helpers
|
||||
//
|
||||
|
||||
function own(el) {
|
||||
return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]'));
|
||||
}
|
||||
|
||||
function pickable(el) {
|
||||
if (!el || el.nodeType !== 1) return false;
|
||||
if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false;
|
||||
if (own(el)) return false;
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.width >= 20 && r.height >= 20;
|
||||
}
|
||||
|
||||
function desc(el) {
|
||||
if (!el) return '';
|
||||
let s = el.tagName.toLowerCase();
|
||||
if (el.id) s += '#' + el.id;
|
||||
else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.');
|
||||
return s;
|
||||
}
|
||||
|
||||
function rectIsUsableAnchor(rect) {
|
||||
return !!rect && rect.width > 0.5 && rect.height > 0.5;
|
||||
}
|
||||
|
||||
function makeFrozenAnchor(el) {
|
||||
if (!el || !el.getBoundingClientRect) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (!rectIsUsableAnchor(r)) return null;
|
||||
const rect = {
|
||||
x: r.x, y: r.y,
|
||||
top: r.top, left: r.left,
|
||||
right: r.right, bottom: r.bottom,
|
||||
width: r.width, height: r.height,
|
||||
};
|
||||
return {
|
||||
__impeccableFrozenAnchor: true,
|
||||
tagName: el.tagName || 'DIV',
|
||||
id: el.id || '',
|
||||
classList: el.classList ? [...el.classList] : [],
|
||||
hasAttribute: () => false,
|
||||
getBoundingClientRect: () => rect,
|
||||
};
|
||||
}
|
||||
|
||||
function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); }
|
||||
|
||||
function cssId(id) {
|
||||
if (window.CSS?.escape) return CSS.escape(id);
|
||||
return String(id).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
|
||||
}
|
||||
|
||||
function liveUiRoot() {
|
||||
const root = window.__IMPECCABLE_LIVE_UI_ROOT__;
|
||||
if (root && typeof root.appendChild === 'function') return root;
|
||||
return document.body;
|
||||
}
|
||||
|
||||
function uiAppend(el) {
|
||||
liveUiRoot().appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function uiAppendStyle(styleEl) {
|
||||
const root = liveUiRoot();
|
||||
if (root && root !== document.body) root.appendChild(styleEl);
|
||||
else document.head.appendChild(styleEl);
|
||||
return styleEl;
|
||||
}
|
||||
|
||||
function uiGetById(id) {
|
||||
const root = liveUiRoot();
|
||||
if (root?.getElementById) {
|
||||
const found = root.getElementById(id);
|
||||
if (found) return found;
|
||||
}
|
||||
if (root?.querySelector) {
|
||||
const found = root.querySelector('#' + cssId(id));
|
||||
if (found) return found;
|
||||
}
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function activeElementDeep() {
|
||||
let active = document.activeElement;
|
||||
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
||||
return active;
|
||||
const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({
|
||||
prefix: PREFIX,
|
||||
skipTags: SKIP_TAGS,
|
||||
document,
|
||||
});
|
||||
if (!domHelpers) {
|
||||
console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.');
|
||||
window.__IMPECCABLE_LIVE_INIT__ = false;
|
||||
return;
|
||||
}
|
||||
const {
|
||||
own,
|
||||
pickable,
|
||||
desc,
|
||||
rectIsUsableAnchor,
|
||||
makeFrozenAnchor,
|
||||
id8,
|
||||
cssId,
|
||||
liveUiRoot,
|
||||
uiAppend,
|
||||
uiAppendStyle,
|
||||
uiGetById,
|
||||
activeElementDeep,
|
||||
defangOutsideHandlers,
|
||||
} = domHelpers;
|
||||
|
||||
window.__IMPECCABLE_LIVE_CHROME_CORE__ = {
|
||||
version: 1,
|
||||
@@ -314,45 +252,6 @@
|
||||
}),
|
||||
};
|
||||
|
||||
// Modal-aware chrome: keep our floating UI clickable inside Radix /
|
||||
// Headless UI / vaul portals.
|
||||
//
|
||||
// Two host-page behaviors break us when the picked element lives inside a
|
||||
// modal dialog:
|
||||
//
|
||||
// 1. Modal scroll-lock disables outside pointer events. Radix's
|
||||
// `DismissableLayer` sets `document.body.style.pointerEvents = 'none'`
|
||||
// while a modal is open and only restores `auto` on the layer. Our
|
||||
// chrome inherits `none` from <body> and becomes unclickable.
|
||||
// 2. The dialog's outside-interaction handler (Radix's
|
||||
// `usePointerDownOutside`) listens at document level and dismisses
|
||||
// the dialog whenever a `pointerdown` lands outside the layer node.
|
||||
// Our chrome is a sibling of <body>, so Radix classifies our clicks
|
||||
// as outside and tears the dialog down mid-task.
|
||||
//
|
||||
// We can't reliably re-parent our chrome into the dialog subtree (z-index
|
||||
// stacking, scroll containers, theming all become host-page concerns), so
|
||||
// we defang both behaviors at our root:
|
||||
//
|
||||
// - `pointer-events: auto !important` overrides the inherited `none`.
|
||||
// - Stop `pointerdown` / `mousedown` propagation so the document-level
|
||||
// dismiss listener never fires for our clicks.
|
||||
// - Stop `focusin` propagation so any focus shifts inside our chrome
|
||||
// don't read as "focus moved outside the dialog" to focus traps.
|
||||
//
|
||||
// Click events still bubble normally - only the early pointer/focus
|
||||
// signals that drive outside-interaction detection are silenced.
|
||||
function defangOutsideHandlers(rootEl, { setPointerEvents = true } = {}) {
|
||||
if (!rootEl) return;
|
||||
if (setPointerEvents) {
|
||||
rootEl.style.setProperty('pointer-events', 'auto', 'important');
|
||||
}
|
||||
const stop = (e) => e.stopPropagation();
|
||||
rootEl.addEventListener('pointerdown', stop);
|
||||
rootEl.addEventListener('mousedown', stop);
|
||||
rootEl.addEventListener('focusin', stop);
|
||||
}
|
||||
|
||||
//
|
||||
// Highlight overlay
|
||||
//
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
|
||||
export const LIVE_BROWSER_SCRIPT_PARTS = Object.freeze([
|
||||
Object.freeze({ name: 'session-state', file: 'live-browser-session.js' }),
|
||||
Object.freeze({ name: 'dom-helpers', file: 'live-browser-dom.js' }),
|
||||
Object.freeze({ name: 'browser-ui', file: 'live-browser.js' }),
|
||||
]);
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const REPO_ROOT = process.cwd();
|
||||
const SCRIPT = join(REPO_ROOT, 'skill/scripts/live-browser-dom.js');
|
||||
|
||||
function createAppendTarget() {
|
||||
return {
|
||||
children: [],
|
||||
appendChild(child) {
|
||||
this.children.push(child);
|
||||
child.parentNode = this;
|
||||
return child;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createElement({
|
||||
id = '',
|
||||
tagName = 'DIV',
|
||||
classList = [],
|
||||
rect = { x: 0, y: 0, top: 0, left: 0, right: 40, bottom: 30, width: 40, height: 30 },
|
||||
closestResult = null,
|
||||
} = {}) {
|
||||
const listeners = {};
|
||||
const styleCalls = [];
|
||||
return {
|
||||
nodeType: 1,
|
||||
id,
|
||||
tagName,
|
||||
classList,
|
||||
listeners,
|
||||
styleCalls,
|
||||
style: {
|
||||
setProperty(...args) { styleCalls.push(args); },
|
||||
},
|
||||
closest() { return closestResult; },
|
||||
getBoundingClientRect() { return rect; },
|
||||
addEventListener(type, handler) { listeners[type] = handler; },
|
||||
};
|
||||
}
|
||||
|
||||
function createDocument() {
|
||||
const elementsById = new Map();
|
||||
const body = createAppendTarget();
|
||||
const head = createAppendTarget();
|
||||
return {
|
||||
body,
|
||||
head,
|
||||
activeElement: null,
|
||||
elementsById,
|
||||
getElementById(id) { return elementsById.get(id) || null; },
|
||||
};
|
||||
}
|
||||
|
||||
function loadFactory(doc = createDocument(), extras = {}) {
|
||||
const context = {
|
||||
document: doc,
|
||||
window: {},
|
||||
globalThis: {},
|
||||
console,
|
||||
CSS: extras.CSS,
|
||||
crypto: extras.crypto,
|
||||
};
|
||||
context.window = context;
|
||||
context.globalThis = context;
|
||||
vm.createContext(context);
|
||||
vm.runInContext(readFileSync(SCRIPT, 'utf-8'), context, { filename: SCRIPT });
|
||||
return { context, createHelpers: context.__IMPECCABLE_LIVE_DOM__.createLiveBrowserDomHelpers };
|
||||
}
|
||||
|
||||
describe('live-browser-dom helpers', () => {
|
||||
it('detects owned chrome and pickable page elements', () => {
|
||||
const doc = createDocument();
|
||||
const { createHelpers } = loadFactory(doc);
|
||||
const helpers = createHelpers({ prefix: 'impeccable-live', skipTags: new Set(['script']) });
|
||||
|
||||
assert.equal(helpers.own(createElement({ id: 'impeccable-live-bar' })), true);
|
||||
assert.ok(helpers.own(createElement({ closestResult: createElement({ id: 'impeccable-live-root' }) })));
|
||||
assert.equal(helpers.pickable(createElement({ tagName: 'SCRIPT' })), false);
|
||||
assert.equal(helpers.pickable(createElement({ rect: { width: 12, height: 30 } })), false);
|
||||
assert.equal(helpers.pickable(createElement({ tagName: 'BUTTON' })), true);
|
||||
});
|
||||
|
||||
it('mounts chrome in the configured live UI root and styles in head by default', () => {
|
||||
const doc = createDocument();
|
||||
const { context, createHelpers } = loadFactory(doc);
|
||||
const helpers = createHelpers({ prefix: 'impeccable-live', document: doc });
|
||||
const uiRoot = createAppendTarget();
|
||||
context.__IMPECCABLE_LIVE_UI_ROOT__ = uiRoot;
|
||||
|
||||
const el = {};
|
||||
const styleInRoot = {};
|
||||
helpers.uiAppend(el);
|
||||
helpers.uiAppendStyle(styleInRoot);
|
||||
assert.deepEqual(uiRoot.children, [el, styleInRoot]);
|
||||
|
||||
context.__IMPECCABLE_LIVE_UI_ROOT__ = null;
|
||||
const styleInHead = {};
|
||||
helpers.uiAppendStyle(styleInHead);
|
||||
assert.deepEqual(doc.head.children, [styleInHead]);
|
||||
});
|
||||
|
||||
it('escapes ids while looking inside the live UI root before document fallback', () => {
|
||||
const doc = createDocument();
|
||||
const rootHit = {};
|
||||
const documentHit = {};
|
||||
doc.elementsById.set('fallback', documentHit);
|
||||
const { context, createHelpers } = loadFactory(doc, { CSS: { escape: (id) => 'escaped-' + id } });
|
||||
context.__IMPECCABLE_LIVE_UI_ROOT__ = {
|
||||
appendChild() {},
|
||||
getElementById() { return null; },
|
||||
querySelector(selector) {
|
||||
assert.equal(selector, '#escaped-a.b');
|
||||
return rootHit;
|
||||
},
|
||||
};
|
||||
const helpers = createHelpers({ prefix: 'impeccable-live', document: doc, css: context.CSS });
|
||||
|
||||
assert.equal(helpers.uiGetById('a.b'), rootHit);
|
||||
context.__IMPECCABLE_LIVE_UI_ROOT__ = null;
|
||||
assert.equal(helpers.uiGetById('fallback'), documentHit);
|
||||
});
|
||||
|
||||
it('freezes usable anchors and follows nested shadow active elements', () => {
|
||||
const doc = createDocument();
|
||||
const inner = { id: 'inner' };
|
||||
doc.activeElement = { shadowRoot: { activeElement: { shadowRoot: { activeElement: inner } } } };
|
||||
const { createHelpers } = loadFactory(doc);
|
||||
const helpers = createHelpers({ prefix: 'impeccable-live', document: doc });
|
||||
const anchor = createElement({ id: 'hero', tagName: 'SECTION', classList: ['hero'] });
|
||||
|
||||
const frozen = helpers.makeFrozenAnchor(anchor);
|
||||
assert.equal(frozen.__impeccableFrozenAnchor, true);
|
||||
assert.equal(frozen.id, 'hero');
|
||||
assert.equal(frozen.classList[0], 'hero');
|
||||
assert.equal(frozen.getBoundingClientRect().width, 40);
|
||||
assert.equal(helpers.makeFrozenAnchor(createElement({ rect: { width: 0, height: 30 } })), null);
|
||||
assert.equal(helpers.activeElementDeep(), inner);
|
||||
});
|
||||
|
||||
it('defangs modal outside handlers on live chrome roots', () => {
|
||||
const { createHelpers } = loadFactory();
|
||||
const helpers = createHelpers({ prefix: 'impeccable-live' });
|
||||
const root = createElement();
|
||||
let stopped = 0;
|
||||
|
||||
helpers.defangOutsideHandlers(root);
|
||||
assert.deepEqual(root.styleCalls[0], ['pointer-events', 'auto', 'important']);
|
||||
root.listeners.pointerdown({ stopPropagation: () => { stopped += 1; } });
|
||||
root.listeners.mousedown({ stopPropagation: () => { stopped += 1; } });
|
||||
root.listeners.focusin({ stopPropagation: () => { stopped += 1; } });
|
||||
assert.equal(stopped, 3);
|
||||
});
|
||||
});
|
||||
@@ -12,11 +12,13 @@ describe('live browser script parts', () => {
|
||||
it('resolves the canonical browser script order', () => {
|
||||
const parts = resolveLiveBrowserScriptParts('/repo/skill/scripts');
|
||||
|
||||
assert.deepEqual(parts.map((part) => part.name), ['session-state', 'browser-ui']);
|
||||
assert.deepEqual(parts.map((part) => part.name), ['session-state', 'dom-helpers', 'browser-ui']);
|
||||
assert.equal(parts[0].file, 'live-browser-session.js');
|
||||
assert.equal(parts[1].file, 'live-browser.js');
|
||||
assert.equal(parts[1].file, 'live-browser-dom.js');
|
||||
assert.equal(parts[2].file, 'live-browser.js');
|
||||
assert.equal(parts[0].path, path.join('/repo/skill/scripts', 'live-browser-session.js'));
|
||||
assert.equal(parts[1].path, path.join('/repo/skill/scripts', 'live-browser.js'));
|
||||
assert.equal(parts[1].path, path.join('/repo/skill/scripts', 'live-browser-dom.js'));
|
||||
assert.equal(parts[2].path, path.join('/repo/skill/scripts', 'live-browser.js'));
|
||||
});
|
||||
|
||||
it('asserts missing script parts by name', () => {
|
||||
@@ -34,6 +36,7 @@ describe('live browser script parts', () => {
|
||||
|
||||
assert.deepEqual(loaded.map((part) => part.source), [
|
||||
'source:live-browser-session.js',
|
||||
'source:live-browser-dom.js',
|
||||
'source:live-browser.js',
|
||||
]);
|
||||
});
|
||||
@@ -45,6 +48,7 @@ describe('live browser script parts', () => {
|
||||
vocabulary: [{ value: 'shape', label: 'Shape' }],
|
||||
parts: [
|
||||
{ name: 'session-state', file: 'live-browser-session.js', source: 'window.__SESSION_PART__ = true;' },
|
||||
{ name: 'dom-helpers', file: 'live-browser-dom.js', source: 'window.__DOM_PART__ = true;' },
|
||||
{ name: 'browser-ui', file: 'live-browser.js', source: 'window.__BROWSER_PART__ = true;' },
|
||||
],
|
||||
});
|
||||
@@ -53,14 +57,17 @@ describe('live browser script parts', () => {
|
||||
const portIndex = script.indexOf('window.__IMPECCABLE_PORT__');
|
||||
const vocabIndex = script.indexOf('window.__IMPECCABLE_VOCAB__');
|
||||
const sessionIndex = script.indexOf('window.__SESSION_PART__');
|
||||
const domIndex = script.indexOf('window.__DOM_PART__');
|
||||
const browserIndex = script.indexOf('window.__BROWSER_PART__');
|
||||
|
||||
assert.ok(tokenIndex !== -1);
|
||||
assert.ok(tokenIndex < portIndex);
|
||||
assert.ok(portIndex < vocabIndex);
|
||||
assert.ok(vocabIndex < sessionIndex);
|
||||
assert.ok(sessionIndex < browserIndex);
|
||||
assert.ok(sessionIndex < domIndex);
|
||||
assert.ok(domIndex < browserIndex);
|
||||
assert.match(script, /impeccable live script part: session-state \(live-browser-session\.js\)/);
|
||||
assert.match(script, /impeccable live script part: dom-helpers \(live-browser-dom\.js\)/);
|
||||
assert.match(script, /impeccable live script part: browser-ui \(live-browser\.js\)/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -230,26 +230,38 @@ describe('live-server integration', () => {
|
||||
assert.ok(text.includes('__IMPECCABLE_PORT__'));
|
||||
const preludeIndex = text.indexOf('window.__IMPECCABLE_VOCAB__');
|
||||
const sessionPartIndex = text.indexOf('impeccable live script part: session-state (live-browser-session.js)');
|
||||
const domPartIndex = text.indexOf('impeccable live script part: dom-helpers (live-browser-dom.js)');
|
||||
const browserPartIndex = text.indexOf('impeccable live script part: browser-ui (live-browser.js)');
|
||||
const sessionHelperIndex = text.indexOf('__IMPECCABLE_LIVE_SESSION__');
|
||||
const domHelperIndex = text.indexOf('__IMPECCABLE_LIVE_DOM__');
|
||||
const browserInitIndex = text.indexOf('__IMPECCABLE_LIVE_INIT__');
|
||||
assert.ok(preludeIndex !== -1);
|
||||
assert.ok(sessionPartIndex !== -1);
|
||||
assert.ok(domPartIndex !== -1);
|
||||
assert.ok(browserPartIndex !== -1);
|
||||
assert.ok(sessionHelperIndex !== -1);
|
||||
assert.ok(domHelperIndex !== -1);
|
||||
assert.ok(browserInitIndex !== -1);
|
||||
assert.ok(
|
||||
preludeIndex < sessionPartIndex,
|
||||
'event=live_server.browser_script_order actor=browser operation=load_live_js risk=prelude_after_script_part expected=prelude before parts actual=' + preludeIndex + ':' + sessionPartIndex,
|
||||
);
|
||||
assert.ok(
|
||||
sessionPartIndex < browserPartIndex,
|
||||
'event=live_server.browser_script_order actor=browser operation=load_live_js risk=browser_part_before_session_helper expected=session part before browser part actual=' + sessionPartIndex + ':' + browserPartIndex,
|
||||
sessionPartIndex < domPartIndex,
|
||||
'event=live_server.browser_script_order actor=browser operation=load_live_js risk=dom_part_before_session_helper expected=session part before dom part actual=' + sessionPartIndex + ':' + domPartIndex,
|
||||
);
|
||||
assert.ok(
|
||||
domPartIndex < browserPartIndex,
|
||||
'event=live_server.browser_script_order actor=browser operation=load_live_js risk=browser_part_before_dom_helpers expected=dom part before browser part actual=' + domPartIndex + ':' + browserPartIndex,
|
||||
);
|
||||
assert.ok(
|
||||
sessionHelperIndex < browserInitIndex,
|
||||
'event=live_server.browser_helper_order actor=browser operation=load_live_js risk=session_helper_missing_before_browser_init expected=session helper before live init actual=' + sessionHelperIndex + ':' + browserInitIndex,
|
||||
);
|
||||
assert.ok(
|
||||
domHelperIndex < browserInitIndex,
|
||||
'event=live_server.browser_helper_order actor=browser operation=load_live_js risk=dom_helper_missing_before_browser_init expected=dom helper before live init actual=' + domHelperIndex + ':' + browserInitIndex,
|
||||
);
|
||||
});
|
||||
|
||||
it('/design-system.json reads DESIGN.md plus .impeccable/design.json', async () => {
|
||||
|
||||
Reference in New Issue
Block a user