49 lines
2.8 KiB
JavaScript
49 lines
2.8 KiB
JavaScript
// Checks that each form label (.tabular forms, query filters) lines up with the field next to it.
|
|
// Usage: node check-form-alignment.mjs (exits 1 if any row is off by more than 2px)
|
|
import { chromium } from 'playwright';
|
|
|
|
const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070';
|
|
const pages = [
|
|
'/projects?display_type=list', '/projects/apollo/issues', '/projects/apollo/time_entries', '/projects/apollo/issues/new', '/issues/13/edit', '/issues/bulk_edit?ids[]=13&ids[]=17',
|
|
'/projects/apollo/time_entries/new', '/settings', '/my/account', '/users/5/edit',
|
|
'/projects/apollo/settings', '/custom_fields/1/edit', '/projects/apollo/wiki/Wiki/edit',
|
|
];
|
|
|
|
const browser = await chromium.launch();
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
|
await page.goto(`${BASE}/login`);
|
|
await page.fill('#username', 'admin');
|
|
await page.fill('#password', 'admin');
|
|
await page.click('#login-submit');
|
|
await page.waitForURL((url) => !url.pathname.startsWith('/login'));
|
|
|
|
let failures = 0;
|
|
for (const path of pages) {
|
|
await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' });
|
|
const rows = await page.evaluate(() => [
|
|
// Query filters: label text vs. operator select
|
|
...[...document.querySelectorAll('#filters-table .filter')].flatMap((row) => {
|
|
const label = row.querySelector('.field label'), control = row.querySelector('.operator select');
|
|
if (!label || !control || !control.offsetParent) return [];
|
|
const l = label.getBoundingClientRect(), c = control.getBoundingClientRect();
|
|
return [{ label: `filter ${label.textContent.trim().slice(0, 22)}`, offset: Math.round(l.top + l.height / 2 - (c.top + c.height / 2)) }];
|
|
}),
|
|
...[...document.querySelectorAll('.tabular p')].flatMap((row) => {
|
|
const label = row.querySelector(':scope > label:not(.floating):not(.block):not(.inline):not(.no-css)');
|
|
const control = row.querySelector(':scope > input:not([type=hidden]), :scope > select, :scope > span > input:not([type=hidden]), :scope > span > select');
|
|
if (!label || !control || !label.offsetParent || !control.offsetParent) return [];
|
|
const style = getComputedStyle(label);
|
|
const labelBox = label.getBoundingClientRect();
|
|
const controlBox = control.getBoundingClientRect();
|
|
const labelMiddle = labelBox.top + parseFloat(style.paddingTop) + parseFloat(style.lineHeight) / 2;
|
|
return [{ label: label.textContent.trim().slice(0, 30), offset: Math.round(labelMiddle - (controlBox.top + controlBox.height / 2)) }];
|
|
}),
|
|
]);
|
|
const bad = rows.filter((r) => Math.abs(r.offset) > 2);
|
|
failures += bad.length;
|
|
console.log(`${bad.length ? 'FAIL' : 'ok '} ${path} (${rows.length} rows)${bad.map((r) => `\n ${r.label}: ${r.offset}px`).join('')}`);
|
|
}
|
|
|
|
await browser.close();
|
|
process.exit(failures ? 1 : 0);
|