const fs = require('fs'); const path = require('path'); const { playwright, storyUrl, captureWithSensors, row, } = require(process.env.HOME + '/astryx/probe-kit/lib.cjs'); const before = { label: 'before-main', port: 6100, root: path.join(process.env.HOME, 'astryx/worktrees/main'), sha: 'd4d4fc58b0bd2e21ad7ab23a492edcb4aafa4bc5', }; const after = { label: 'after-pr', port: 6258, root: path.join(process.env.HOME, 'astryx/worktrees/review-pr-5558-badge-truncate'), sha: '1733f9bf784f7a491409e0b59c1d789b83d8ac03', }; const outDir = path.join(after.root, 'review-evidence/pr-5558'); fs.mkdirSync(outDir, {recursive: true}); const story = 'core-badge--default'; function makeUrl(port, label, globals) { const base = storyUrl(story, port, globals); const args = `label:${encodeURIComponent(label)}`; return `${base}&args=${args}`; } async function open(port, label, {rtl = false, viewport = {width: 420, height: 220}} = {}) { const browser = await playwright.chromium.launch(); const page = await browser.newPage({viewport}); const sensorErrors = []; page.on('pageerror', error => sensorErrors.push(`pageerror: ${error.message}`)); page.on('console', message => { if (message.type() === 'error') sensorErrors.push(`console: ${message.text()}`); }); await page.goto(makeUrl(port, label, rtl ? {direction: 'rtl'} : undefined), { waitUntil: 'domcontentloaded', timeout: 30000, }); await page.waitForSelector('.astryx-badge', {timeout: 15000}); await page.waitForFunction( expected => document.querySelector('.astryx-badge')?.innerText === expected, label, {timeout: 15000}, ); if (rtl) { await page.waitForFunction(() => document.querySelector('[dir="rtl"]') != null, null, {timeout: 10000}); } await page.evaluate(() => document.fonts.ready); return {browser, page, sensorErrors}; } async function arrange(page, scenario) { await page.evaluate(s => { const root = document.querySelector('#storybook-root'); const badge = root.querySelector('.astryx-badge'); const direction = getComputedStyle(badge).direction; root.style.margin = '24px'; root.style.font = '14px system-ui'; root.dataset.reviewCase = s.name; if (s.type === 'table') { const table = document.createElement('table'); table.id = 'review-case'; table.style.tableLayout = 'fixed'; table.style.width = `${s.totalWidth}px`; table.style.borderCollapse = 'collapse'; const tr = document.createElement('tr'); const td1 = document.createElement('td'); td1.id = 'badge-cell'; td1.style.width = `${s.badgeWidth}px`; td1.style.outline = '2px dashed rgb(80,80,80)'; td1.style.padding = '4px'; td1.style.verticalAlign = 'middle'; const td2 = document.createElement('td'); td2.id = 'neighbor-cell'; td2.textContent = 'Next cell text'; td2.style.padding = '4px'; td2.style.verticalAlign = 'middle'; td2.style.background = 'rgba(255, 215, 0, 0.35)'; td1.appendChild(badge); tr.append(td1, td2); table.appendChild(tr); root.replaceChildren(table); } else if (s.type === 'flex') { const box = document.createElement('div'); box.id = 'review-case'; box.style.display = 'flex'; box.style.width = `${s.totalWidth}px`; box.style.outline = '2px dashed rgb(80,80,80)'; box.style.gap = '8px'; box.style.alignItems = 'center'; const sib = document.createElement('span'); sib.id = 'neighbor-cell'; sib.textContent = 'Next'; sib.style.background = 'rgba(255, 215, 0, 0.35)'; box.append(badge, sib); root.replaceChildren(box); } else if (s.type === 'grid') { const box = document.createElement('div'); box.id = 'review-case'; box.style.display = 'grid'; box.style.gridTemplateColumns = `${s.badgeWidth}px 1fr`; box.style.width = `${s.totalWidth}px`; box.style.outline = '2px dashed rgb(80,80,80)'; box.style.columnGap = '8px'; const sib = document.createElement('span'); sib.id = 'neighbor-cell'; sib.textContent = 'Next'; sib.style.background = 'rgba(255, 215, 0, 0.35)'; box.append(badge, sib); root.replaceChildren(box); } else if (s.type === 'auto') { const box = document.createElement('div'); box.id = 'review-case'; box.style.display = 'inline-block'; box.style.outline = '2px dashed rgb(80,80,80)'; box.style.padding = '4px'; box.appendChild(badge); root.replaceChildren(box); } else if (s.type === 'plain') { const box = document.createElement('div'); box.id = 'review-case'; box.style.width = `${s.badgeWidth}px`; box.style.outline = '2px dashed rgb(80,80,80)'; box.style.padding = '4px'; box.appendChild(badge); root.replaceChildren(box); } const caseEl = document.querySelector('#review-case'); if (caseEl) { caseEl.style.background = 'rgb(255,255,255)'; caseEl.dir = direction; } }, scenario); await page.waitForTimeout(150); } async function measureArm(arm, scenario, {capture = false, rtl = false} = {}) { const {browser, page, sensorErrors} = await open(arm.port, scenario.labelText, {rtl, viewport: scenario.viewport || {width: 420, height: 220}}); await arrange(page, scenario); const metrics = await page.evaluate(() => { const badge = document.querySelector('.astryx-badge'); const labelEl = badge?.querySelector('span:not([aria-hidden])'); const container = document.querySelector('#review-case') || document.querySelector('#storybook-root'); const neighbor = document.querySelector('#neighbor-cell'); const badgeRect = badge.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); const neighborRect = neighbor?.getBoundingClientRect() || null; const badgeStyle = getComputedStyle(badge); const labelStyle = labelEl ? getComputedStyle(labelEl) : null; return { text: badge.innerText, rootWidth: Number(badgeRect.width.toFixed(2)), rootHeight: Number(badgeRect.height.toFixed(2)), containerWidth: Number(containerRect.width.toFixed(2)), spillRight: Number(Math.max(0, badgeRect.right - containerRect.right).toFixed(2)), spillLeft: Number(Math.max(0, containerRect.left - badgeRect.left).toFixed(2)), overflowX: Number(Math.max(0, badge.scrollWidth - badge.clientWidth).toFixed(2)), labelOverflowX: labelEl ? Number(Math.max(0, labelEl.scrollWidth - labelEl.clientWidth).toFixed(2)) : null, neighborOverlap: neighborRect ? Number(Math.max(0, badgeRect.right - neighborRect.left).toFixed(2)) : null, hasInnerLabelSpan: !!labelEl, rootMaxWidth: badgeStyle.maxWidth, rootMinWidth: badgeStyle.minWidth, rootOverflow: badgeStyle.overflow, labelOverflow: labelStyle?.overflow || null, labelTextOverflow: labelStyle?.textOverflow || null, labelMinWidth: labelStyle?.minWidth || null, titleAttr: badge.getAttribute('title'), tabIndex: badge.getAttribute('tabindex'), focusable: badge.tabIndex >= 0, direction: badgeStyle.direction, }; }); let receiptPath = null; let imagePath = null; if (capture) { imagePath = path.join(outDir, `${arm.label}__${scenario.name}.png`); const expected = { globals: rtl ? {direction: 'rtl'} : {}, themeAttr: 'neutral', colorMode: 'light', direction: rtl ? 'rtl' : 'ltr', viewport: {...(scenario.viewport || {width: 420, height: 220}), dpr: 1}, media: {forcedColors: false, reducedMotion: false, coarsePointer: false, hover: true}, targetCount: 1, state: {case: scenario.name, text: scenario.labelText}, runningAnimations: 0, surfaceClass: 'light', }; await captureWithSensors(page, imagePath, { label: `${arm.label} ${scenario.name}`, buildRoot: arm.root, expectedSha: arm.sha, story, target: '.astryx-badge', surface: '#review-case', expected, sensorErrors, readState: async p => p.evaluate(() => ({ case: document.querySelector('#storybook-root').dataset.reviewCase, text: document.querySelector('.astryx-badge').innerText, })), screenshot: {fullPage: true}, }); receiptPath = `${imagePath}.sensors.json`; } await browser.close(); return {...metrics, arm: arm.label, scenario: scenario.name, imagePath, receiptPath}; } async function main() { const scenarios = [ {name: 'table-long-100', type: 'table', labelText: 'Awaiting security review', badgeWidth: 100, totalWidth: 240, capture: true}, {name: 'table-short-control-300', type: 'table', labelText: 'Active', badgeWidth: 300, totalWidth: 420, capture: true, viewport: {width: 520, height: 220}}, {name: 'plain-long-100', type: 'plain', labelText: 'Awaiting security review', badgeWidth: 100}, {name: 'plain-unbroken-100', type: 'plain', labelText: 'AwaitingSecurityReviewWithoutSpaces', badgeWidth: 100}, {name: 'flex-long-170', type: 'flex', labelText: 'Awaiting security review', totalWidth: 170}, {name: 'grid-long-100', type: 'grid', labelText: 'Awaiting security review', badgeWidth: 100, totalWidth: 240}, {name: 'auto-long', type: 'auto', labelText: 'Awaiting security review'}, ]; const rows = []; for (const scenario of scenarios) { for (const arm of [before, after]) { rows.push(await measureArm(arm, scenario, {capture: scenario.capture})); } } // RTL via Storybook globals on the constraining case, measurement only. const rtlScenario = {name: 'rtl-plain-long-100', type: 'plain', labelText: 'Awaiting security review', badgeWidth: 100}; rows.push(await measureArm(before, rtlScenario, {rtl: true})); rows.push(await measureArm(after, rtlScenario, {rtl: true})); fs.writeFileSync(path.join(outDir, 'badge-overflow-measurements.json'), JSON.stringify(rows, null, 2) + '\n'); for (const r of rows) row(`${r.scenario}:${r.arm}`, { width: r.rootWidth, container: r.containerWidth, spillR: r.spillRight, spillL: r.spillLeft, overflowX: r.overflowX, labelOverflowX: r.labelOverflowX, overlap: r.neighborOverlap, inner: r.hasInnerLabelSpan, rootMax: r.rootMaxWidth, rootMin: r.rootMinWidth, labelOverflow: r.labelOverflow, labelTextOverflow: r.labelTextOverflow, title: r.titleAttr === null ? 'none' : r.titleAttr, focusable: r.focusable, dir: r.direction, }); } main().catch(error => { console.error(error); process.exit(1); });