569d887d1e
Renaming the package so any further PRs directed to the website are targeted to the reworked code instead of diverging. Once merged, I will start preparing this for deployment to dev to test before releasing to prod. Any improvements will also be applied to this package. I avoided making significant changes to API routes so nothing breaks, but will test it thoroughly today to confirm. That said, everything is ported - double checked. Big diff PR, impossible to review, but last one! No more rebuilds.
194 lines
7.9 KiB
JavaScript
194 lines
7.9 KiB
JavaScript
import { chromium } from 'playwright';
|
|
|
|
// OLD-parity battery: computed values measured identically on :3002 and
|
|
// :3004 with selectors that resolve on BOTH DOMs. Exact equality except
|
|
// the executable ledger below.
|
|
const OLD_URL = process.env.MOCKUP_OLD_URL ?? 'http://localhost:3002/';
|
|
const NEW_URL = process.env.VISUAL_BATTERY_URL ?? 'http://localhost:3004/';
|
|
|
|
// Every deliberate divergence is declared here; an unledgered diff fails,
|
|
// a ledgered-but-absent diff also fails.
|
|
// (Empty today: the one candidate — product transparent.medium p3 0.078
|
|
// vs old rgba 0.08 — sits below colorimetric resolution and passes as
|
|
// equal.) Shape: [{ key, reason }].
|
|
const LEDGER = [];
|
|
|
|
const failures = [];
|
|
const assert = (condition, message) => {
|
|
console.log(` ${condition ? '✓' : '✗'} ${message}`);
|
|
if (!condition) {
|
|
failures.push(message);
|
|
}
|
|
};
|
|
|
|
async function measure(browser, url) {
|
|
const page = await browser.newPage({
|
|
viewport: { width: 1440, height: 950 },
|
|
deviceScaleFactor: 1,
|
|
});
|
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 240000 });
|
|
await page.waitForTimeout(4000);
|
|
const values = await page.evaluate(() => {
|
|
// page.evaluate serializes this callback: helpers must live inside it.
|
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
const styleOf = (el) => (el ? getComputedStyle(el) : null);
|
|
// page.evaluate serializes this callback: helpers must live inside it.
|
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
const round = (n) => Math.round(n * 10) / 10;
|
|
// The framed window: both sites render a 20px-radius white shell.
|
|
const frame = [...document.querySelectorAll('div')].find((el) => {
|
|
const cs = getComputedStyle(el);
|
|
const rect = el.getBoundingClientRect();
|
|
return cs.borderRadius === '20px' && rect.width > 900;
|
|
});
|
|
const sidebar = frame?.querySelector('aside');
|
|
const companiesLabel = [...(sidebar?.querySelectorAll('span') ?? [])].find(
|
|
(el) => el.textContent === 'Companies',
|
|
);
|
|
const anthropicRow = frame?.querySelector('[data-row-id="anthropic"]');
|
|
const firstCell = anthropicRow?.firstElementChild;
|
|
const chip = [...(anthropicRow?.querySelectorAll('div') ?? [])].find(
|
|
(el) => el.textContent?.trim() === 'Anthropic',
|
|
);
|
|
const workspaceName = [...(sidebar?.querySelectorAll('span') ?? [])].find(
|
|
(el) => el.textContent === 'Apple',
|
|
);
|
|
const newButton = [...(frame?.querySelectorAll('div, span') ?? [])].find(
|
|
(el) =>
|
|
el.textContent?.trim() === 'New' &&
|
|
styleOf(el.parentElement)?.borderRadius === '4px',
|
|
);
|
|
const terminal = document.querySelector('[data-terminal-shell]');
|
|
const terminalRect = terminal?.getBoundingClientRect();
|
|
const terminalStyle = styleOf(terminal);
|
|
const sendButton = terminal?.querySelector(
|
|
'button[aria-label="Send message"]',
|
|
);
|
|
const workspaceChip = [
|
|
...(terminal?.querySelectorAll('button') ?? []),
|
|
].find((el) => el.textContent?.includes('my-twenty-app'));
|
|
const promptText = [...(terminal?.querySelectorAll('p') ?? [])].find((el) =>
|
|
el.textContent?.startsWith('Scaffold a launch-ops CRM'),
|
|
);
|
|
const toggle = terminal?.querySelector('[role="tablist"]');
|
|
const activeSegment = toggle?.querySelector('[aria-selected="true"]');
|
|
const frameStyle = styleOf(frame);
|
|
const frameRect = frame?.getBoundingClientRect();
|
|
return {
|
|
frameWidth: frameRect ? round(frameRect.width) : null,
|
|
frameRadius: frameStyle?.borderRadius ?? null,
|
|
frameBorder: frameStyle?.borderTopWidth ?? null,
|
|
sidebarWidth: sidebar
|
|
? round(sidebar.getBoundingClientRect().width)
|
|
: null,
|
|
navLabelFont: styleOf(companiesLabel)?.fontSize ?? null,
|
|
navLabelWeight: styleOf(companiesLabel)?.fontWeight ?? null,
|
|
navLabelColor: styleOf(companiesLabel)?.color ?? null,
|
|
rowHeight: firstCell
|
|
? round(firstCell.getBoundingClientRect().height)
|
|
: null,
|
|
cellBorderBottom: styleOf(firstCell)?.borderBottomColor ?? null,
|
|
chipHeight: chip ? round(chip.getBoundingClientRect().height) : null,
|
|
chipRadius: styleOf(chip)?.borderRadius ?? null,
|
|
chipBackground: styleOf(chip)?.backgroundColor ?? null,
|
|
workspaceNameFont: styleOf(workspaceName)?.fontSize ?? null,
|
|
workspaceNameColor: styleOf(workspaceName)?.color ?? null,
|
|
navActionBorderColor: newButton
|
|
? styleOf(newButton.parentElement)?.borderTopColor
|
|
: null,
|
|
terminalWidth: terminalRect ? round(terminalRect.width) : null,
|
|
terminalHeight: terminalRect ? round(terminalRect.height) : null,
|
|
terminalRadius: terminalStyle?.borderRadius ?? null,
|
|
terminalBackground: terminalStyle?.backgroundColor ?? null,
|
|
terminalBorderColor: terminalStyle?.borderTopColor ?? null,
|
|
sendBackground: styleOf(sendButton)?.backgroundColor ?? null,
|
|
sendRadius: styleOf(sendButton)?.borderRadius ?? null,
|
|
workspaceChipBackground: styleOf(workspaceChip)?.backgroundColor ?? null,
|
|
workspaceChipColor: styleOf(workspaceChip)?.color ?? null,
|
|
workspaceChipHeight: workspaceChip
|
|
? round(workspaceChip.getBoundingClientRect().height)
|
|
: null,
|
|
promptFont: styleOf(promptText)?.fontSize ?? null,
|
|
promptColor: styleOf(promptText)?.color ?? null,
|
|
promptLineHeight: styleOf(promptText)?.lineHeight ?? null,
|
|
toggleBackground: styleOf(toggle)?.backgroundColor ?? null,
|
|
toggleRadius: styleOf(toggle)?.borderRadius ?? null,
|
|
activeSegmentBackground: styleOf(activeSegment)?.backgroundColor ?? null,
|
|
activeSegmentShadow: styleOf(activeSegment)?.boxShadow ?? null,
|
|
activeSegmentFont: styleOf(activeSegment)?.fontSize ?? null,
|
|
};
|
|
});
|
|
await page.close();
|
|
return values;
|
|
}
|
|
|
|
const browser = await chromium.launch({ channel: 'chrome', headless: true });
|
|
const oldValues = await measure(browser, OLD_URL);
|
|
const newValues = await measure(browser, NEW_URL);
|
|
await browser.close();
|
|
|
|
// Colors compare colorimetrically: the old site serializes sRGB, ours
|
|
// display-p3 — identical colors, different encodings.
|
|
function normalizeColor(value) {
|
|
if (typeof value !== 'string') {
|
|
return value;
|
|
}
|
|
const p3 = value.match(
|
|
/color\(display-p3 ([\d.]+) ([\d.]+) ([\d.]+)(?: \/ ([\d.]+))?\)/,
|
|
);
|
|
if (p3) {
|
|
const [, r, g, b, a] = p3;
|
|
return [r, g, b]
|
|
.map((channel) => Math.round(Number(channel) * 255))
|
|
.concat([a === undefined ? 1 : Math.round(Number(a) * 1000) / 1000])
|
|
.join(',');
|
|
}
|
|
const rgb = value.match(
|
|
/rgba?\(([\d.]+), ([\d.]+), ([\d.]+)(?:, ([\d.]+))?\)/,
|
|
);
|
|
if (rgb) {
|
|
const [, r, g, b, a] = rgb;
|
|
return [r, g, b]
|
|
.map((channel) => Math.round(Number(channel)))
|
|
.concat([a === undefined ? 1 : Math.round(Number(a) * 1000) / 1000])
|
|
.join(',');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function colorsEqual(a, b) {
|
|
const left = normalizeColor(a).split(',').map(Number);
|
|
const right = normalizeColor(b).split(',').map(Number);
|
|
if (left.length !== right.length || left.some(Number.isNaN)) {
|
|
return normalizeColor(a) === normalizeColor(b);
|
|
}
|
|
return left.every(
|
|
(channel, index) =>
|
|
Math.abs(channel - right[index]) <= (index === 3 ? 0.005 : 1),
|
|
);
|
|
}
|
|
|
|
const ledgeredKeys = new Set(LEDGER.map((entry) => entry.key));
|
|
for (const key of Object.keys(oldValues)) {
|
|
const oldValue = oldValues[key];
|
|
const newValue = newValues[key];
|
|
const isColorKey = /color|background|border(?!.*Width)/i.test(key);
|
|
const equal = isColorKey
|
|
? colorsEqual(String(oldValue), String(newValue))
|
|
: String(oldValue) === String(newValue);
|
|
if (ledgeredKeys.has(key)) {
|
|
assert(
|
|
!equal,
|
|
`${key}: ledgered divergence present (old ${oldValue} vs new ${newValue})`,
|
|
);
|
|
continue;
|
|
}
|
|
assert(equal, `${key}: ${oldValue} == ${newValue}`);
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(`mockup-old-parity: FAILED (${failures.length})`);
|
|
process.exit(1);
|
|
}
|
|
console.log('mockup-old-parity: OK');
|