levkin.ca/scripts/test-stack-folder.mjs
ilia 6949bee6e6 Add SEO baseline + rebuild www/ (mobile toc/prefs polish, responsive viewport)
SEO: robots.txt, sitemap.xml, canonical, OG/Twitter tags, JSON-LD
(ProfessionalService) on / and /folders/, plus a reused screenshot as
og-image.png. prepare-www.mjs now copies robots/sitemap/og-image into
the build. Also carries pending mobile layout tweaks (toc/prefs bar,
viewport-fit=cover) that were already staged in the working tree.
2026-07-08 10:58:46 -04:00

125 lines
4.4 KiB
JavaScript

/**
* Tab rail alignment tests for /stack-folder/ (desktop + mobile).
* Run: STACK_URL=http://localhost:5175/stack-folder/ npm run test:folder
*/
import { chromium, devices } from 'playwright';
const URL = process.env.STACK_URL || 'http://localhost:5175/stack-folder/';
const VERBOSE = process.env.VERBOSE === '1';
async function runViewport(name, contextOptions) {
const browser = await chromium.launch();
const page = await browser.newPage(contextOptions);
await page.goto(URL, { waitUntil: 'networkidle' });
async function tabTops() {
return page.evaluate(() => {
const tabs = [...document.querySelectorAll('.mount .tab')];
const stick =
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--stack-stick')) * 16;
return {
scrollY: window.scrollY,
isFolded: document.querySelector('.mount')?.classList.contains('is-folded'),
stick,
tabs: tabs.map((t) => ({
code: t.querySelector('.tab-code')?.textContent,
top: Math.round(t.getBoundingClientRect().top),
left: Math.round(t.getBoundingClientRect().left),
})),
};
});
}
if (VERBOSE) console.log(`${name} initial`, await tabTops());
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
await page.waitForTimeout(600);
if (VERBOSE) console.log(`${name} max scroll`, await tabTops());
const railVisible = await page.evaluate(() => {
const rail = document.querySelector('.tab-rail');
if (!rail) return false;
return getComputedStyle(rail).display !== 'none';
});
if (name === 'mobile' && !railVisible) {
await browser.close();
return [`${name}: mobile tab rail not visible`];
}
const foldIssues = await page.evaluate(({ isMobile }) => {
const tabs = [...document.querySelectorAll('.mount .tab')];
const tops = tabs.map((t) => t.getBoundingClientRect().top);
const min = Math.min(...tops);
const max = Math.max(...tops);
const issues = [];
const spreadLimit = isMobile ? 40 : 30;
if (document.documentElement.scrollWidth > window.innerWidth + 1) {
issues.push('horizontal overflow');
}
if (max - min > spreadLimit) {
issues.push(`tab row spread ${Math.round(max - min)}px (want ≤${spreadLimit})`);
}
if (!document.querySelector('.mount')?.classList.contains('is-folded')) {
issues.push('mount not folded at max scroll');
}
return issues;
}, { isMobile: name === 'mobile' });
if (foldIssues.length) {
await browser.close();
return foldIssues.map((issue) => `${name}: ${issue}`);
}
if (name === 'mobile') {
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(200);
await page.locator('.tab-rail .rail-tab[data-goto="3"]').tap();
await page.waitForTimeout(800);
if (VERBOSE) console.log(`${name} rail goto L3`, await tabTops());
} else {
await page.click('[data-goto="7"]');
await page.waitForTimeout(800);
if (VERBOSE) console.log(`${name} goto L7`, await tabTops());
}
const fail = await page.evaluate(({ isMobile }) => {
const issues = [];
if (isMobile) {
const rail = document.querySelector('.tab-rail .rail-tab[data-goto="3"]');
const railBox = rail?.getBoundingClientRect();
if (!railBox || railBox.height < 40) issues.push('rail tap target too small');
if (window.scrollY < 100) issues.push('rail L3 tap did not scroll');
} else {
const tabs = [...document.querySelectorAll('.mount .tab')];
const tops = tabs.map((t) => t.getBoundingClientRect().top);
const l7 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L7');
const l0 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L0');
if (l7 && l0 && Math.abs(l7.getBoundingClientRect().top - l0.getBoundingClientRect().top) > 30) {
issues.push('L7 not aligned with L0');
}
}
return issues;
}, { isMobile: name === 'mobile' });
await browser.close();
return fail.map((issue) => `${name}: ${issue}`);
}
const desktop = { viewport: { width: 1400, height: 900 } };
const mobile = { ...devices['iPhone 13'], hasTouch: true };
const results = [
...(await runViewport('desktop', desktop)),
...(await runViewport('mobile', mobile)),
];
if (results.length) {
console.error('FAIL:', results.join('; '));
process.exit(1);
}
console.log('PASS: stack-folder (desktop + mobile)');