Files
twenty/packages/twenty-website/scripts/page-lock.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

213 lines
6.5 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
createBattery,
launchBrowser,
NEW_BASE,
openPage,
} from './battery-kit.mjs';
// Locks approved pages against THEMSELVES: a structured snapshot of the
// page's composition (sections, rhythm, intros, headings, CTAs, visual
// slots) committed as fixtures. Porting further pages must not move any
// of it; a deliberate change is re-recorded and reviewed as a fixture
// diff in the commit.
//
// node scripts/page-lock.mjs verify against fixtures
// node scripts/page-lock.mjs --record re-record fixtures
const LOCKED_PAGES = ['/', '/product'];
// One viewport inside each breakpoint tier (sm 768 / md 921 / lg 1281),
// so every responsive variant of the approved pages is under lock —
// phone stacks and swipe decks, the sm band, the md band where the
// hero window crops, and the full desktop layout.
const LOCKED_VIEWPORTS = [
{ label: 'base-390', width: 390, height: 844 },
{ label: 'sm-820', width: 820, height: 900 },
{ label: 'md-1100', width: 1100, height: 900 },
{ label: 'lg-1440', width: 1440, height: 900 },
];
const FIXTURES_DIR = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'locks',
);
const isRecording = process.argv.includes('--record');
function fixturePath(pagePath) {
const name = pagePath === '/' ? 'home' : pagePath.replaceAll('/', '');
return path.join(FIXTURES_DIR, `${name}.json`);
}
// The page's structural fingerprint: everything the house rules govern,
// read with stable selectors (no text anchors that break on copy edits
// — except headings, which ARE content under lock).
function readPageSnapshot(page) {
return page.evaluate(() => {
const sections = [...document.querySelectorAll('section')].map((el) => {
const previous = el.previousElementSibling;
return {
scheme: el.getAttribute('data-scheme'),
rhythm: el.getAttribute('data-rhythm'),
paddingTop: getComputedStyle(el).paddingTop,
paddingBottom: getComputedStyle(el).paddingBottom,
background: getComputedStyle(el).backgroundColor,
followsSameScheme:
previous?.tagName === 'SECTION' &&
previous.getAttribute('data-scheme') ===
el.getAttribute('data-scheme'),
};
});
const eyebrows = [...document.querySelectorAll('p')]
.filter(
(el) =>
el.querySelector('span[aria-hidden]') &&
el.textContent.trim().length < 40,
)
.map((row) => {
const block = row.parentElement;
const heading =
block.querySelector('h1, h2, h3') ??
block.parentElement.querySelector('h1, h2, h3');
const rect = row.getBoundingClientRect();
const parentRect = row.parentElement.getBoundingClientRect();
return {
label: row.textContent.trim(),
gapToHeading: heading
? Math.round(heading.getBoundingClientRect().top - rect.bottom)
: null,
centered:
Math.abs(
rect.left - parentRect.left - (parentRect.right - rect.right),
) <= 2,
};
});
const headings = [...document.querySelectorAll('h1, h2')].map((el) => {
const headingStyle = getComputedStyle(el);
return {
tag: el.tagName,
text: el.textContent.trim().slice(0, 60),
fontSize: headingStyle.fontSize,
fontWeight: headingStyle.fontWeight,
textAlign: headingStyle.textAlign,
};
});
const visualSlots = [...document.querySelectorAll('[data-illustration]')]
.map((el) => el.getAttribute('data-illustration'))
.toSorted();
const ctas = [...document.querySelectorAll('main a')]
.filter((el) => el.querySelector('svg, span[aria-hidden]'))
.slice(0, 12)
.map((el) => ({
label: el.textContent.trim().slice(0, 30),
href: el.getAttribute('href'),
}));
return {
sections,
eyebrows,
headings,
visualSlots,
ctas,
sectionCount: sections.length,
};
});
}
function diffSnapshots(battery, pagePath, expected, actual) {
const walk = (keyPath, expectedValue, actualValue) => {
const expectedText = JSON.stringify(expectedValue);
const actualText = JSON.stringify(actualValue);
if (expectedText === actualText) return true;
if (
Array.isArray(expectedValue) &&
Array.isArray(actualValue) &&
expectedValue.length === actualValue.length
) {
let allEqual = true;
expectedValue.forEach((item, index) => {
if (!walk(`${keyPath}[${index}]`, item, actualValue[index])) {
allEqual = false;
}
});
return allEqual;
}
battery.fail(
`${pagePath} ${keyPath}`,
`locked ${expectedText} vs live ${actualText}`,
);
return false;
};
let clean = true;
for (const key of Object.keys(expected)) {
if (!walk(key, expected[key], actual[key])) clean = false;
}
if (clean) {
battery.ok(
`${pagePath} matches its lock`,
`${expected.sectionCount} sections, ${expected.eyebrows.length} eyebrows, ${expected.headings.length} headings`,
);
}
}
const battery = createBattery('page-lock');
const browser = await launchBrowser();
for (const pagePath of LOCKED_PAGES) {
const snapshots = {};
for (const viewport of LOCKED_VIEWPORTS) {
// eslint-disable-next-line no-await-in-loop
const page = await openPage(browser, `${NEW_BASE}${pagePath}`, {
settleMs: 1200,
viewport,
});
// eslint-disable-next-line no-await-in-loop
snapshots[viewport.label] = await readPageSnapshot(page);
// eslint-disable-next-line no-await-in-loop
await page.close();
}
if (isRecording) {
fs.mkdirSync(FIXTURES_DIR, { recursive: true });
fs.writeFileSync(
fixturePath(pagePath),
`${JSON.stringify(snapshots, null, 2)}\n`,
);
battery.ok(`${pagePath} recorded`, fixturePath(pagePath));
continue;
}
if (!fs.existsSync(fixturePath(pagePath))) {
battery.fail(`${pagePath} lock exists`, 'run with --record first');
continue;
}
const expected = JSON.parse(fs.readFileSync(fixturePath(pagePath), 'utf8'));
for (const viewport of LOCKED_VIEWPORTS) {
if (!expected[viewport.label]) {
battery.fail(
`${pagePath} ${viewport.label} lock exists`,
're-record (new viewport tier)',
);
continue;
}
diffSnapshots(
battery,
`${pagePath} @${viewport.label}`,
expected[viewport.label],
snapshots[viewport.label],
);
}
}
await battery.finish(browser);