Files
upstream-twenty/packages/twenty-website/scripts/mockup-old-parity.mjs
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05: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');