150 lines
7.8 KiB
JavaScript
150 lines
7.8 KiB
JavaScript
// Split-view navigation check: the real pane-nav runtime (src/inject/paneNav.ts) over the static
|
|
// x.com fixtures of the theme matrix, in Chromium, at split-pane widths. Per width it asserts that a
|
|
// redundant pane parks X's rail off-screen and out of the layout, re-centers the main column, slides
|
|
// the rail in after resting on the left edge (not while crossing it), and parks it again after the
|
|
// pointer leaves; a lead pane (redundantNav false) and a disabled feature leave X untouched.
|
|
// Screenshots go to verification/pane-nav/. The fixtures approximate x.com: not a live X test.
|
|
//
|
|
// node scripts/pane-nav-check.mjs
|
|
|
|
import { build } from 'esbuild';
|
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { join } from 'node:path';
|
|
import { chromium } from 'playwright';
|
|
import { fixtureHtml } from './theme-matrix/fixtures.mjs';
|
|
|
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
const out = join(root, '../../verification/pane-nav');
|
|
await mkdir(out, { recursive: true });
|
|
|
|
const bundle = (await build({
|
|
stdin: { contents: `export { installPaneNav } from '${process.env.PANE_NAV_SRC ?? './src/inject/paneNav.ts'}';`, resolveDir: root, loader: 'ts' },
|
|
bundle: true, format: 'iife', globalName: 'BTPane', write: false, target: 'chrome120',
|
|
})).outputFiles[0].text;
|
|
|
|
const failures = [];
|
|
const check = (label, ok, detail = '') => {
|
|
console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ` (${detail})` : ''}`);
|
|
if (!ok) failures.push(label);
|
|
};
|
|
|
|
const browser = await chromium.launch();
|
|
// The runtime is installed like the app does it: an init script, before the parser creates <html>.
|
|
const context = await browser.newContext({ viewport: { width: 800, height: 760 } });
|
|
await context.addInitScript({ content: `${bundle};window.pane = BTPane.installPaneNav(window);` });
|
|
const page = await context.newPage();
|
|
// X's classic rail (header[role=banner]) and a plain-div rail, the fallback path findRail() marks itself.
|
|
const html = fixtureHtml('home', 'dark');
|
|
const VARIANTS = {
|
|
header: { html, rail: 'header[role="banner"]' },
|
|
div: {
|
|
html: html.replace('<header role="banner" class="css-175oi2r"', '<div class="css-175oi2r fx-rail"').replace('</header>', '</div>')
|
|
.replace(`document.querySelector('header[role="banner"]')`, `document.querySelector('.fx-rail')`),
|
|
rail: '.fx-rail',
|
|
},
|
|
};
|
|
if (VARIANTS.div.html.includes('role="banner"')) throw new Error('div variant still has a banner header');
|
|
|
|
let railSelector = '';
|
|
const railBox = () => page.evaluate(selector => {
|
|
const rail = document.querySelector(selector);
|
|
const box = rail.getBoundingClientRect();
|
|
const style = getComputedStyle(rail);
|
|
return { left: box.left, right: box.right, width: box.width, position: style.position, visibility: style.visibility };
|
|
}, railSelector);
|
|
const columnGap = () => page.evaluate(() => {
|
|
const row = document.querySelector('main[role="main"] > div').getBoundingClientRect();
|
|
return { left: Math.round(row.left), right: Math.round(innerWidth - row.right) };
|
|
});
|
|
const state = () => page.evaluate(() => document.documentElement.getAttribute('data-bt-pane-nav'));
|
|
const setMenu = open => page.evaluate(open => {
|
|
let layers = document.getElementById('layers');
|
|
if (!layers) { layers = document.createElement('div'); layers.id = 'layers'; document.body.appendChild(layers); }
|
|
layers.querySelector('.fake-menu')?.remove();
|
|
if (open) layers.insertAdjacentHTML('beforeend', '<div class="fake-menu" role="menu" style="position:fixed;right:40px;top:120px;width:200px;height:80px"></div>');
|
|
}, open);
|
|
|
|
for (const [variant, width] of [['header', 800], ['header', 1100], ['header', 1400], ['div', 800], ['div', 1400]]) {
|
|
const label = `${variant} ${width}`;
|
|
const file = join(out, `fixture-${variant}.html`);
|
|
railSelector = VARIANTS[variant].rail;
|
|
await writeFile(file, VARIANTS[variant].html);
|
|
await page.setViewportSize({ width, height: 760 });
|
|
await page.goto(pathToFileURL(file).href);
|
|
const before = await railBox();
|
|
await page.screenshot({ path: join(out, `${variant}-${width}-0-lead.png`) });
|
|
|
|
await page.evaluate(() => window.pane.set({ redundantNav: false }));
|
|
check(`${label}: lead pane untouched`, (await state()) === null && (await railBox()).position !== 'fixed');
|
|
|
|
await page.evaluate(() => window.pane.set({ redundantNav: true }));
|
|
await page.waitForTimeout(350);
|
|
const parked = await railBox();
|
|
const gap = await columnGap();
|
|
check(`${label}: rail parked off-screen`, parked.position === 'fixed' && parked.right <= 1 && parked.visibility === 'hidden', JSON.stringify(parked));
|
|
check(`${label}: rail keeps X's column width (${before.width > 300 ? 'flex header' : 'icon column'})`, parked.width >= 60 && parked.width <= 320, `width ${parked.width}`);
|
|
check(`${label}: main column centered in the freed width`, Math.abs(gap.left - gap.right) <= 2, JSON.stringify(gap));
|
|
await page.screenshot({ path: join(out, `${variant}-${width}-1-parked.png`) });
|
|
|
|
// Crossing the edge quickly does not open it.
|
|
await page.mouse.move(400, 300);
|
|
await page.mouse.move(4, 300);
|
|
await page.mouse.move(400, 300);
|
|
await page.waitForTimeout(250);
|
|
check(`${label}: crossing the edge does not peek`, (await state()) === 'hidden');
|
|
|
|
await page.mouse.move(4, 300);
|
|
await page.waitForTimeout(450);
|
|
const peek = await railBox();
|
|
check(`${label}: resting on the edge slides the rail in`, (await state()) === 'peek' && Math.abs(peek.left) <= 1 && peek.visibility === 'visible', JSON.stringify(peek));
|
|
const overlay = await columnGap();
|
|
check(`${label}: slid-in rail overlays, the column does not move`, overlay.left === gap.left, JSON.stringify(overlay));
|
|
await page.mouse.move(40, 300);
|
|
await page.waitForTimeout(600);
|
|
check(`${label}: stays while the pointer is on the rail`, (await state()) === 'peek');
|
|
await page.screenshot({ path: join(out, `${variant}-${width}-2-peek.png`) });
|
|
|
|
await page.mouse.move(Math.round(width * 0.6), 300);
|
|
await page.waitForTimeout(700);
|
|
check(`${label}: leaves after the pointer does`, (await state()) === 'hidden' && (await railBox()).visibility === 'hidden');
|
|
|
|
await page.mouse.move(4, 300);
|
|
await page.waitForTimeout(450);
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(300);
|
|
check(`${label}: Escape parks it`, (await state()) === 'hidden');
|
|
await page.mouse.move(Math.round(width * 0.6), 300);
|
|
|
|
// A menu somewhere else on the page (a post's ··· menu) must not hold the rail out.
|
|
await page.mouse.move(4, 300);
|
|
await page.waitForTimeout(450);
|
|
await page.mouse.move(Math.round(width * 0.6), 300, { steps: 12 });
|
|
await setMenu(true);
|
|
await page.waitForTimeout(700);
|
|
check(`${label}: an unrelated open menu does not hold the rail`, (await state()) === 'hidden');
|
|
await setMenu(false);
|
|
|
|
// One the rail opened (More, account switcher) does, until it closes.
|
|
await page.mouse.move(4, 300);
|
|
await page.waitForTimeout(450);
|
|
await page.mouse.move(40, 740);
|
|
await page.mouse.down();
|
|
await page.mouse.up();
|
|
await setMenu(true);
|
|
await page.mouse.move(Math.round(width * 0.6), 300);
|
|
await page.waitForTimeout(700);
|
|
check(`${label}: the rail's own menu holds it`, (await state()) === 'peek');
|
|
await setMenu(false);
|
|
await page.waitForTimeout(500);
|
|
check(`${label}: parks once that menu closes`, (await state()) === 'hidden');
|
|
|
|
await page.evaluate(() => window.pane.setEnabled(false));
|
|
const restored = await railBox();
|
|
check(`${label}: feature off restores X's layout`, (await state()) === null && restored.position !== 'fixed' && Math.abs(restored.width - before.width) <= 1, JSON.stringify(restored));
|
|
check(`${label}: edge and style removed`, await page.evaluate(() => !document.getElementById('bt-rail-edge') && !document.getElementById('bt-pane-nav')));
|
|
}
|
|
|
|
await browser.close();
|
|
console.log(failures.length ? `\n${failures.length} failed` : '\nall passed');
|
|
process.exit(failures.length ? 1 : 0);
|