File size: 7,757 Bytes
39371ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | import { chromium } from 'playwright';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
const url = process.argv[2] ?? 'http://127.0.0.1:4173/';
const result = { url, checks: [], errors: [] };
const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
page.setDefaultTimeout(15000);
page.on('pageerror', error => result.errors.push(error.message));
const ready = async () => { await page.waitForFunction(() => window.browserPi?.terminal); await page.evaluate(() => window.browserPi.ready); };
const divider = page.getByRole('separator', { name: 'Resize chat and workspace' });
const geometry = async () => {
const size = await page.evaluate(() => {
const rect = selector => document.querySelector(selector).getBoundingClientRect().toJSON();
return { main: rect('main'), chat: rect('.conversation'), workspace: rect('.workspace'), handle: rect('#column-resizer'),
desktop: matchMedia('(min-width: 851px)').matches,
gutter: parseFloat(getComputedStyle(document.querySelector('main')).getPropertyValue('--content-gutter')),
cols: window.browserPi.terminal.cols, overflow: document.documentElement.scrollWidth > innerWidth };
});
if (size.desktop) {
closeTo(size.workspace.left, size.chat.right);
closeTo(size.workspace.right + size.gutter, size.main.right);
}
return size;
};
const frames = () => page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
async function drag(delta, release = true) {
const r = await divider.boundingBox();
await page.mouse.move(r.x + r.width / 2, r.y + r.height / 2); await page.mouse.down();
await page.mouse.move(r.x + r.width / 2 + delta, r.y + r.height / 2, { steps: 12 });
if (release) await page.mouse.up();
await frames();
}
const closeTo = (actual, expected) => assert(Math.abs(actual - expected) < 1, `${actual} vs ${expected}`);
try {
await page.goto(url); await ready();
assert(await divider.isVisible());
const initial = await geometry();
closeTo(initial.chat.width, initial.main.width / 1.83);
assert(initial.handle.x >= initial.chat.right - 14.1 && initial.handle.right <= initial.workspace.left + .1);
await page.locator('.xterm-helper-textarea').focus();
await page.keyboard.type('Keep this draft'); await page.keyboard.press('Shift+Enter'); await page.keyboard.type('while resizing');
await page.locator('#files button[title="/workspace/products.json"]').click();
await page.locator('#editor').fill('{"unsaved": true}');
await page.locator('#command').fill('echo "unsent command"');
await drag(240);
assert((await geometry()).chat.width > initial.chat.width + 230);
await page.waitForFunction(cols => window.browserPi.terminal.cols > cols, initial.cols);
assert.equal(await page.evaluate(() => window.browserPi.terminal.text), 'Keep this draft\nwhile resizing');
assert.equal(await page.locator('#editor').inputValue(), '{"unsaved": true}');
assert.equal(await page.locator('#command').inputValue(), 'echo "unsent command"');
assert(await page.locator('#save').isEnabled());
await page.waitForFunction(() => document.querySelector('#editor-highlight').clientWidth === document.querySelector('#editor').clientWidth);
result.checks.push('Live resizing preserves Pi, file and shell drafts and resizes the terminal and syntax layer');
await drag(-2000); closeTo((await geometry()).chat.width, 420);
await divider.press('End'); await frames();
let size = await geometry(); closeTo(size.main.width - size.chat.width, 370);
assert.equal(await divider.getAttribute('aria-valuenow'), await divider.getAttribute('aria-valuemax'));
await divider.press('ArrowLeft'); await frames(); closeTo((await geometry()).chat.width, size.chat.width - 16);
await divider.press('Shift+ArrowLeft'); await frames(); closeTo((await geometry()).chat.width, size.chat.width - 64);
await divider.press('Home'); await frames(); closeTo((await geometry()).chat.width, 420);
await divider.press('Enter'); await frames(); closeTo((await geometry()).chat.width, initial.chat.width);
result.checks.push('Width limits, keyboard nudges, Shift, Home/End, Enter reset and accessible values work');
await drag(150, false); await divider.press('Escape'); await page.mouse.up(); await frames();
closeTo((await geometry()).chat.width, initial.chat.width);
assert(!await page.locator('html').evaluate(el => el.classList.contains('is-resizing-columns')));
await drag(100, false); await divider.dispatchEvent('pointercancel', { pointerId: 1 }); await page.mouse.up(); await frames();
closeTo((await geometry()).chat.width, initial.chat.width);
await drag(170); const saved = (await geometry()).chat.width;
await page.reload(); await ready(); await frames(); closeTo((await geometry()).chat.width, saved);
await page.setViewportSize({ width: 851, height: 800 }); await frames();
size = await geometry(); assert(size.chat.width >= 420 && size.main.width - size.chat.width >= 369.9 && !size.overflow);
await page.setViewportSize({ width: 1440, height: 1000 }); await frames(); closeTo((await geometry()).chat.width, saved);
result.checks.push('Cancelled drags roll back; saved ratio survives reload and returns after narrow-window clamping');
await divider.dblclick(); await frames(); closeTo((await geometry()).chat.width, initial.chat.width);
await page.setViewportSize({ width: 2400, height: 1000 }); await frames();
size = await geometry(); assert.equal(size.main.width, 1800); assert(size.main.left > 0);
await drag(100); closeTo((await geometry()).chat.width, size.chat.width + 100);
await page.screenshot({ path: '../validation/column-resizer-wide.png', fullPage: true });
await page.setViewportSize({ width: 1440, height: 1000 }); await frames(); await divider.dblclick(); await frames();
await page.screenshot({ path: '../validation/column-resizer-desktop.png', fullPage: true });
await drag(80, false); await page.setViewportSize({ width: 850, height: 800 }); await page.mouse.up(); await frames();
assert(!await page.locator('html').evaluate(el => el.classList.contains('is-resizing-columns')));
for (const width of [850,390,320]) {
await page.setViewportSize({ width, height: 844 }); await frames();
assert(await page.locator('#column-resizer').isHidden());
size = await geometry(); assert(!size.overflow && size.workspace.top >= size.chat.bottom - 1);
}
await page.screenshot({ path: '../validation/column-resizer-mobile.png', fullPage: true });
result.checks.push('Double-click resets; centered wide layouts drag accurately; mobile stays stacked and cancels desktop drags');
const blocked = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
await blocked.addInitScript(() => { Storage.prototype.getItem = Storage.prototype.setItem = () => { throw new DOMException('Blocked', 'SecurityError'); }; });
await blocked.goto(url); await blocked.waitForFunction(() => window.browserPi?.terminal);
await blocked.getByRole('separator', { name: 'Resize chat and workspace' }).press('Home');
closeTo(await blocked.locator('.conversation').evaluate(el => el.getBoundingClientRect().width), 420);
await blocked.close(); result.checks.push('Resizing works with blocked browser storage');
assert.deepEqual(result.errors, []); result.passed = true; console.log(JSON.stringify(result));
} catch (error) {
result.failure = error.stack;
await page.screenshot({ path: '../validation/column-resizer-failure.png', fullPage: true }); throw error;
} finally {
await fs.writeFile('../validation/column-resizer.json', JSON.stringify(result, null, 2)); await browser.close();
}
|