mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dc88f670d |
@@ -162,6 +162,7 @@ export const SUITES = {
|
||||
'tests/live-svelte-component-accept.test.mjs',
|
||||
'tests/live-tanstack-adapter.test.mjs',
|
||||
'tests/live-target-context.test.mjs',
|
||||
'tests/live-ui-surfaces.test.mjs',
|
||||
'tests/live-wrap.test.mjs',
|
||||
'tests/live-wrap-buffer-aware.test.mjs',
|
||||
],
|
||||
|
||||
@@ -97,23 +97,20 @@
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = [
|
||||
{ key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] },
|
||||
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
|
||||
{ key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] },
|
||||
{ key: 'action-picker', ids: [PREFIX + '-picker'] },
|
||||
{ key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] },
|
||||
{ key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] },
|
||||
{ key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] },
|
||||
{ key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] },
|
||||
{ key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] },
|
||||
{ key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] },
|
||||
{ key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] },
|
||||
{ key: 'design-system-panel', ids: [PREFIX + '-design-host'] },
|
||||
{ key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] },
|
||||
{ key: 'css-isolation-boundary', ids: [PREFIX + '-root'] },
|
||||
];
|
||||
// The Live chrome inventory (which surfaces exist, and the element ids each
|
||||
// one owns) comes from the canonical source, skill/scripts/live/ui-surfaces.mjs,
|
||||
// which the /live.js assembler serializes into these globals alongside the
|
||||
// token/port/vocabulary. This file is served raw and injected as a classic
|
||||
// script, so it cannot import that module; the private impeccable-site repo
|
||||
// imports it directly to check its Live UI lab holds a snapshot for every
|
||||
// surface, which only works while the list has exactly one definition.
|
||||
// Add a surface in ui-surfaces.mjs, not here.
|
||||
const LIVE_CHROME_MOUNT_CONTRACT = Array.isArray(window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__)
|
||||
? window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__
|
||||
: ['root', 'transport', 'state', 'actions'];
|
||||
const LIVE_UI_SURFACES = Array.isArray(window.__IMPECCABLE_LIVE_UI_SURFACES__)
|
||||
? window.__IMPECCABLE_LIVE_UI_SURFACES__
|
||||
: [];
|
||||
const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))];
|
||||
|
||||
//
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { LIVE_CHROME_MOUNT_CONTRACT, LIVE_UI_SURFACES } from './ui-surfaces.mjs';
|
||||
|
||||
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' }),
|
||||
@@ -32,7 +34,20 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re
|
||||
}));
|
||||
}
|
||||
|
||||
export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', appRoot = null, parts }) {
|
||||
export function assembleLiveBrowserScript({
|
||||
token,
|
||||
port,
|
||||
vocabulary,
|
||||
commandPrefix = '/',
|
||||
appRoot = null,
|
||||
parts,
|
||||
// Defaulted rather than threaded through live-server.mjs: the browser bundle
|
||||
// must always carry the canonical inventory, and a default makes that true by
|
||||
// construction instead of by every caller remembering to pass it. Overridable
|
||||
// so tests can assemble with a stand-in.
|
||||
uiSurfaces = LIVE_UI_SURFACES,
|
||||
mountContract = LIVE_CHROME_MOUNT_CONTRACT,
|
||||
}) {
|
||||
const prelude =
|
||||
`window.__IMPECCABLE_TOKEN__ = '${token}';\n` +
|
||||
`window.__IMPECCABLE_PORT__ = ${port};\n` +
|
||||
@@ -44,7 +59,14 @@ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPref
|
||||
`window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` +
|
||||
// Canonical command vocabulary (values + labels + icons). live-browser.js
|
||||
// builds its action picker from this instead of an inline copy.
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`;
|
||||
`window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n` +
|
||||
// Canonical Live chrome inventory from live/ui-surfaces.mjs. live-browser.js
|
||||
// is a classic script and cannot import an ES module at runtime, so the list
|
||||
// is serialized here and read off the global there. Node consumers (this
|
||||
// repo's tests, the impeccable-site Live UI lab) import the module directly,
|
||||
// which is what keeps the two from drifting.
|
||||
`window.__IMPECCABLE_LIVE_UI_SURFACES__ = ${JSON.stringify(uiSurfaces)};\n` +
|
||||
`window.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = ${JSON.stringify(mountContract)};\n`;
|
||||
|
||||
const body = parts.map((part) => {
|
||||
const file = part.file || path.basename(part.path || '');
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Canonical inventory of the Live overlay's UI surfaces: one entry per piece of
|
||||
* chrome Live mounts on the user's page, with the element ids that make it up.
|
||||
*
|
||||
* Single source of truth, consumed by:
|
||||
* - skill/scripts/live/browser-script-parts.mjs — serializes this into
|
||||
* window.__IMPECCABLE_LIVE_UI_SURFACES__ in the /live.js prelude.
|
||||
* - skill/scripts/live-browser.js — publishes it on
|
||||
* window.__IMPECCABLE_LIVE_CHROME_CORE__ for adapters and E2E probes. That
|
||||
* file is served raw and injected as a classic <script>, so it cannot
|
||||
* import this module at runtime; it reads the injected global instead, the
|
||||
* same path live/vocabulary.mjs already takes for the command palette.
|
||||
* - the private impeccable-site repo — site/components/LiveUiGallery.astro
|
||||
* and tests/live-ui-lab.test.mjs import LIVE_UI_SURFACES at build time and
|
||||
* fail the site build when the Live UI lab has no snapshot for a surface
|
||||
* defined here. That guard only guards if it reads this list rather than a
|
||||
* copy the site keeps, so this module must stay importable from Node.
|
||||
* Renaming a key or the module is a breaking change for that build; the
|
||||
* list was briefly inlined into live-browser.js and the site had to parse
|
||||
* it back out with a regex.
|
||||
*
|
||||
* Add a surface here and both the browser bundle and the site lab follow.
|
||||
*/
|
||||
|
||||
/** Id prefix every Live chrome element carries. Mirrored by PREFIX in live-browser.js. */
|
||||
export const LIVE_UI_PREFIX = 'impeccable-live';
|
||||
|
||||
const id = (suffix) => `${LIVE_UI_PREFIX}-${suffix}`;
|
||||
|
||||
/**
|
||||
* The mount contract every Live chrome adapter (DOM, Svelte, ...) satisfies.
|
||||
* Published alongside the surfaces on __IMPECCABLE_LIVE_CHROME_CORE__.
|
||||
*/
|
||||
export const LIVE_CHROME_MOUNT_CONTRACT = Object.freeze(['root', 'transport', 'state', 'actions']);
|
||||
|
||||
export const LIVE_UI_SURFACES = Object.freeze([
|
||||
{
|
||||
key: 'global-bottom-bar',
|
||||
ids: [
|
||||
id('global-bar'), id('global-bar-brand'), id('pick-toggle'), id('insert-toggle'),
|
||||
id('detect-toggle'), id('detect-badge'), id('design-toggle'), id('page-chat'),
|
||||
id('page-chat-input'), id('page-chat-voice'), id('page-chat-send'),
|
||||
],
|
||||
},
|
||||
{ key: 'pending-copy-edit-dock', ids: [id('pending-dock')] },
|
||||
{
|
||||
key: 'element-selection-chrome',
|
||||
ids: [
|
||||
id('highlight'), id('tooltip'), id('bar'), id('selection-pill'), id('input'),
|
||||
id('configure-voice'), id('configure-bar-tooltip'),
|
||||
],
|
||||
},
|
||||
{ key: 'action-picker', ids: [id('picker')] },
|
||||
{ key: 'edit-chrome', ids: [id('edit-badge')] },
|
||||
{ key: 'generating-row', ids: [id('bar'), id('shader')] },
|
||||
{ key: 'variant-cycling-row', ids: [id('bar'), id('params-panel')] },
|
||||
{ key: 'variant-params-panel', ids: [id('params-panel')] },
|
||||
{ key: 'saving-confirmed-rows', ids: [id('bar')] },
|
||||
{
|
||||
key: 'insert-mode-chrome',
|
||||
ids: [
|
||||
id('insert-line'), id('insert-placeholder'), id('placeholder-resize'), id('insert-input'),
|
||||
id('insert-voice'), id('insert-create'), id('insert-create-tooltip'),
|
||||
],
|
||||
},
|
||||
{ key: 'annotation-chrome', ids: [id('annot'), id('annot-svg'), id('annot-pins'), id('annot-clear')] },
|
||||
{ key: 'design-system-panel', ids: [id('design-host')] },
|
||||
{ key: 'toasts-and-errors', ids: [id('toast'), id('mount-error')] },
|
||||
{ key: 'css-isolation-boundary', ids: [id('root')] },
|
||||
].map((surface) => Object.freeze({ ...surface, ids: Object.freeze(surface.ids) })));
|
||||
|
||||
/** Every id any surface owns, de-duplicated, in surface order. */
|
||||
export const LIVE_UI_COMPONENT_IDS = Object.freeze([
|
||||
...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids)),
|
||||
]);
|
||||
@@ -18,6 +18,8 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { LIVE_UI_COMPONENT_IDS, LIVE_UI_PREFIX } from '../skill/scripts/live/ui-surfaces.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const LIVE_BROWSER = path.resolve(
|
||||
__dirname,
|
||||
@@ -1280,9 +1282,12 @@ describe('live-browser.js regression guards', () => {
|
||||
/if \(e\.key === 'Enter'\) \{\s*e\.preventDefault\(\);\s*submitSteerMessage\(\);/,
|
||||
'keyboard submit stays',
|
||||
);
|
||||
assert.match(
|
||||
SOURCE,
|
||||
/PREFIX \+ '-page-chat-send'\] \}/,
|
||||
// The inventory used to be an inline literal here, so this used to be a
|
||||
// source-text match on the surface entry. It now lives in
|
||||
// live/ui-surfaces.mjs (importable, one definition), so assert membership
|
||||
// in the real list instead of the shape of the line that declares it.
|
||||
assert.ok(
|
||||
LIVE_UI_COMPONENT_IDS.includes(`${LIVE_UI_PREFIX}-page-chat-send`),
|
||||
'the Send control must be registered as live UI chrome so it is excluded from capture',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -200,6 +200,17 @@ describe('live-server integration', () => {
|
||||
assert.deepEqual(injected, LIVE_COMMANDS);
|
||||
});
|
||||
|
||||
it('/live.js injects the canonical Live UI surface inventory', async () => {
|
||||
// Same path as the vocabulary: live-browser.js is a classic script and
|
||||
// cannot import live/ui-surfaces.mjs, so the served bundle must carry the
|
||||
// module's list. Node consumers (including the impeccable-site Live UI lab)
|
||||
// import the module, and this is what keeps the two the same list.
|
||||
const { LIVE_UI_SURFACES } = await import('../skill/scripts/live/ui-surfaces.mjs');
|
||||
const body = await (await fetch(`http://localhost:${server.port}/live.js?token=${server.token}`)).text();
|
||||
const injected = JSON.parse(body.match(/window\.__IMPECCABLE_LIVE_UI_SURFACES__\s*=\s*(\[.*?\]);\n/s)[1]);
|
||||
assert.deepEqual(injected, JSON.parse(JSON.stringify(LIVE_UI_SURFACES)));
|
||||
});
|
||||
|
||||
it('/status returns durable recovery state', async () => {
|
||||
await drainPolls(server);
|
||||
const eventRes = await fetch(`http://localhost:${server.port}/events`, {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* The Live chrome inventory has exactly one definition, and it is importable.
|
||||
*
|
||||
* live-browser.js is served raw as a classic script and cannot import an ES
|
||||
* module, which is why the list was once inlined there as a function-scope
|
||||
* const. That put it out of reach of every Node consumer: this repo's own
|
||||
* tooling, and the private impeccable-site repo, whose Live UI lab fails its
|
||||
* build when a surface defined here has no snapshot. That guard is only a guard
|
||||
* while it reads Live's real list, so these tests pin both halves: the module
|
||||
* stays the definition, and the browser bundle still receives it at runtime.
|
||||
*/
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { assembleLiveBrowserScript } from '../skill/scripts/live/browser-script-parts.mjs';
|
||||
import {
|
||||
LIVE_CHROME_MOUNT_CONTRACT,
|
||||
LIVE_UI_COMPONENT_IDS,
|
||||
LIVE_UI_PREFIX,
|
||||
LIVE_UI_SURFACES,
|
||||
} from '../skill/scripts/live/ui-surfaces.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const LIVE_BROWSER_SOURCE = fs.readFileSync(
|
||||
path.resolve(__dirname, '..', 'skill/scripts/live-browser.js'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
describe('live UI surfaces module', () => {
|
||||
it('exports a non-empty inventory with unique keys', () => {
|
||||
assert.ok(LIVE_UI_SURFACES.length > 0);
|
||||
const keys = LIVE_UI_SURFACES.map((surface) => surface.key);
|
||||
assert.equal(new Set(keys).size, keys.length, 'surface keys must be unique');
|
||||
for (const surface of LIVE_UI_SURFACES) {
|
||||
assert.equal(typeof surface.key, 'string');
|
||||
assert.ok(surface.ids.length > 0, `${surface.key} must own at least one element id`);
|
||||
}
|
||||
});
|
||||
|
||||
it('builds every id from the canonical prefix', () => {
|
||||
for (const id of LIVE_UI_COMPONENT_IDS) {
|
||||
assert.ok(id.startsWith(`${LIVE_UI_PREFIX}-`), `${id} must carry the ${LIVE_UI_PREFIX} prefix`);
|
||||
}
|
||||
});
|
||||
|
||||
it('agrees with the PREFIX live-browser.js hardcodes', () => {
|
||||
// live-browser.js cannot import LIVE_UI_PREFIX, so it declares the string
|
||||
// itself and every id it creates at runtime is built from that. If the two
|
||||
// drift, the inventory names ids that no element on the page ever has.
|
||||
const declared = LIVE_BROWSER_SOURCE.match(/const PREFIX = '([^']+)';/);
|
||||
assert.ok(declared, 'live-browser.js must declare `const PREFIX = ...`');
|
||||
assert.equal(declared[1], LIVE_UI_PREFIX);
|
||||
});
|
||||
|
||||
it('is not redefined inside live-browser.js', () => {
|
||||
// The list living in two places is the failure this module exists to end.
|
||||
assert.doesNotMatch(LIVE_BROWSER_SOURCE, /const LIVE_UI_SURFACES = \[/);
|
||||
assert.match(LIVE_BROWSER_SOURCE, /window\.__IMPECCABLE_LIVE_UI_SURFACES__/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('live UI surfaces reach the browser bundle', () => {
|
||||
const assemble = () => assembleLiveBrowserScript({
|
||||
token: 'token-a',
|
||||
port: 8421,
|
||||
vocabulary: [],
|
||||
parts: [{ name: 'browser-ui', file: 'live-browser.js', source: LIVE_BROWSER_SOURCE }],
|
||||
});
|
||||
|
||||
it('serializes the canonical inventory into the prelude by default', () => {
|
||||
const script = assemble();
|
||||
const surfaces = JSON.parse(
|
||||
script.match(/window\.__IMPECCABLE_LIVE_UI_SURFACES__ = (\[.*?\]);\n/s)[1],
|
||||
);
|
||||
assert.deepEqual(surfaces, JSON.parse(JSON.stringify(LIVE_UI_SURFACES)));
|
||||
|
||||
const contract = JSON.parse(
|
||||
script.match(/window\.__IMPECCABLE_LIVE_MOUNT_CONTRACT__ = (\[.*?\]);\n/s)[1],
|
||||
);
|
||||
assert.deepEqual(contract, [...LIVE_CHROME_MOUNT_CONTRACT]);
|
||||
});
|
||||
|
||||
it('injects the globals before the script part that reads them', () => {
|
||||
const script = assemble();
|
||||
assert.ok(
|
||||
script.indexOf('window.__IMPECCABLE_LIVE_UI_SURFACES__ =')
|
||||
< script.indexOf('impeccable live script part: browser-ui'),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user