Files
twenty/packages/twenty-website/scripts/mockup-old-parity.mjs
T
Abdullah. 569d887d1e [Website] Cut over to the rebuilt site (#21825)
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.
2026-06-19 10:22:46 +02:00

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');