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
169 lines
5.9 KiB
JavaScript
169 lines
5.9 KiB
JavaScript
import {
|
|
createBattery,
|
|
launchBrowser,
|
|
NEW_BASE,
|
|
OLD_BASE,
|
|
VIEWPORT,
|
|
} from './battery-kit.mjs';
|
|
|
|
// A/B battery for the product demo closer: centered intro, the emergent
|
|
// two-line heading break, CTA chrome, the pattern backdrop, and the
|
|
// static (terminal-less) mockup.
|
|
|
|
const { compare, fail, finish, ok } = createBattery('product-demo-parity');
|
|
|
|
const browser = await launchBrowser();
|
|
|
|
async function readDemo(base) {
|
|
const page = await browser.newPage({ viewport: VIEWPORT });
|
|
await page.goto(`${base}/product`, { waitUntil: 'load', timeout: 240000 });
|
|
await page.waitForTimeout(1200);
|
|
// Park on the section first so the lazy pattern image loads.
|
|
await page.evaluate(() => {
|
|
const heading = [...document.querySelectorAll('h2')].find((el) =>
|
|
el.textContent.includes('thousand words'),
|
|
);
|
|
heading?.scrollIntoView({ block: 'start' });
|
|
});
|
|
await page.waitForTimeout(1200);
|
|
const state = await page.evaluate(() => {
|
|
const heading = [...document.querySelectorAll('h2')].find((el) =>
|
|
el.textContent.includes('thousand words'),
|
|
);
|
|
if (!heading) return null;
|
|
heading.scrollIntoView({ block: 'center' });
|
|
const headingStyle = getComputedStyle(heading);
|
|
|
|
// First-line text: walk characters until the line top jumps.
|
|
const range = document.createRange();
|
|
const walker = document.createTreeWalker(heading, NodeFilter.SHOW_TEXT);
|
|
let firstLineTop = null;
|
|
let firstLine = '';
|
|
let node;
|
|
outer: while ((node = walker.nextNode())) {
|
|
for (let index = 0; index < node.textContent.length; index += 1) {
|
|
range.setStart(node, index);
|
|
range.setEnd(node, index + 1);
|
|
const rect = range.getBoundingClientRect();
|
|
if (rect.width === 0) continue;
|
|
if (firstLineTop === null) firstLineTop = rect.top;
|
|
if (Math.abs(rect.top - firstLineTop) > 5) break outer;
|
|
firstLine += node.textContent[index];
|
|
}
|
|
}
|
|
|
|
const root = heading.closest('section') ?? heading.closest('div');
|
|
const cta = [...root.querySelectorAll('a')].find((el) =>
|
|
el.textContent.includes('Try Twenty Cloud'),
|
|
);
|
|
const ctaStyle = cta ? getComputedStyle(cta) : null;
|
|
|
|
// The pattern layer is a sibling of the section on the old site;
|
|
// search from the shared wrapper.
|
|
const wrapper = root.parentElement ?? root;
|
|
const pattern = [...wrapper.querySelectorAll('img')].find((img) =>
|
|
decodeURIComponent(img.currentSrc).includes('product/demo/background'),
|
|
);
|
|
const patternLayer = pattern?.closest('div[aria-hidden]');
|
|
const patternStyle = patternLayer ? getComputedStyle(patternLayer) : null;
|
|
|
|
const ctaToMockupGap = (() => {
|
|
const ctaRect = cta?.getBoundingClientRect();
|
|
if (!ctaRect) return null;
|
|
const frameBelow = [...document.querySelectorAll('main div')].find(
|
|
(el) => {
|
|
const rect = el.getBoundingClientRect();
|
|
return (
|
|
rect.width >= 1000 &&
|
|
rect.top > ctaRect.bottom &&
|
|
getComputedStyle(el).borderRadius === '20px'
|
|
);
|
|
},
|
|
);
|
|
return frameBelow
|
|
? Math.round(frameBelow.getBoundingClientRect().top - ctaRect.bottom)
|
|
: null;
|
|
})();
|
|
|
|
// The static mockup window: the 20px-radius frame at the scene's
|
|
// 1040x676 (old aspect 1280/832 reduces to the same 20:13).
|
|
const frame = [...document.querySelectorAll('div')].find((el) => {
|
|
const rect = el.getBoundingClientRect();
|
|
return (
|
|
rect.width >= 900 &&
|
|
rect.top > heading.getBoundingClientRect().top &&
|
|
getComputedStyle(el).borderRadius === '20px' &&
|
|
Math.abs(rect.width / rect.height - 20 / 13) < 0.01 &&
|
|
el.textContent.includes('Companies')
|
|
);
|
|
});
|
|
|
|
return {
|
|
ctaToMockupGap,
|
|
firstLine: firstLine.trim(),
|
|
headingStyle: `${headingStyle.fontSize} ${headingStyle.fontWeight} ${headingStyle.textAlign}`,
|
|
ctaHref: cta?.getAttribute('href') ?? null,
|
|
ctaInk: ctaStyle ? `${ctaStyle.backgroundColor} ${ctaStyle.color}` : null,
|
|
patternLoaded: Boolean(pattern && pattern.naturalWidth > 0),
|
|
patternOpacity: patternStyle?.opacity ?? null,
|
|
mockupPresent: Boolean(frame),
|
|
mockupRows: frame
|
|
? frame.textContent.includes('Anthropic') &&
|
|
frame.textContent.includes('Stripe')
|
|
: false,
|
|
};
|
|
});
|
|
await page.close();
|
|
return state;
|
|
}
|
|
|
|
const oldDemo = await readDemo(OLD_BASE);
|
|
const newDemo = await readDemo(NEW_BASE);
|
|
|
|
if (!oldDemo || !newDemo) {
|
|
fail(
|
|
'demo section present on both sites',
|
|
`old=${Boolean(oldDemo)} new=${Boolean(newDemo)}`,
|
|
);
|
|
} else {
|
|
compare(
|
|
'heading first line breaks after "a"',
|
|
oldDemo.firstLine,
|
|
newDemo.firstLine,
|
|
);
|
|
compare('heading style', oldDemo.headingStyle, newDemo.headingStyle);
|
|
compare('CTA href', oldDemo.ctaHref, newDemo.ctaHref);
|
|
compare('CTA chrome', oldDemo.ctaInk, newDemo.ctaInk);
|
|
compare(
|
|
'pattern backdrop at 0.6 over the lower stage',
|
|
`${oldDemo.patternLoaded} ${oldDemo.patternOpacity}`,
|
|
`${newDemo.patternLoaded} ${newDemo.patternOpacity}`,
|
|
);
|
|
compare(
|
|
'static mockup with the companies fiction',
|
|
`${oldDemo.mockupPresent} ${oldDemo.mockupRows}`,
|
|
`${newDemo.mockupPresent} ${newDemo.mockupRows}`,
|
|
);
|
|
|
|
// RATIFIED DIVERGENCE (user, 2026-06-13): the demo hangs the mockup at
|
|
// the hero's CTA-to-window measure (68px token) instead of the old
|
|
// demo's looser stack spacing — the two CTA-over-mockup moments read
|
|
// the same. Asserting the difference keeps it deliberate.
|
|
if (
|
|
newDemo.ctaToMockupGap === 68 &&
|
|
oldDemo.ctaToMockupGap !== newDemo.ctaToMockupGap
|
|
) {
|
|
ok(
|
|
'demo mockup hangs at the hero CTA gap (ledgered divergence)',
|
|
`old ${oldDemo.ctaToMockupGap}px vs new ${newDemo.ctaToMockupGap}px`,
|
|
);
|
|
} else {
|
|
fail(
|
|
'demo mockup hangs at the hero CTA gap (ledgered divergence)',
|
|
`old ${oldDemo.ctaToMockupGap}px vs new ${newDemo.ctaToMockupGap}px`,
|
|
);
|
|
}
|
|
}
|
|
|
|
await finish(browser);
|