Files

95 lines
5.4 KiB
JavaScript

// Reports visible elements that still carry core Redmine shapes the theme should have replaced:
// 1-3px corners (core uses 3px, M3 uses 4/8/12/16/28px or full), core's grey box shadow
// (rgba of --oc-gray-9, theme shadows never use it), square boxes outlined on all four sides
// (core's card style, e.g. the old .mypage-box), and dashed or dotted borders.
// Checks desktop and mobile, because core's responsive.css brings its own shapes. Exits 1 on any hit.
// Usage: node check-core-leaks.mjs [--verbose]
import { chromium } from 'playwright';
const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070';
const pages = [
'/my/page', '/projects', '/projects?display_type=list', '/projects/apollo', '/projects/apollo/issues', '/issues/13',
'/projects/apollo/issues/new', '/issues/13/edit', '/issues/bulk_edit?ids[]=13&ids[]=17', '/projects/apollo/issues/gantt',
'/projects/apollo/issues/calendar', '/projects/apollo/roadmap', '/versions/2', '/projects/apollo/activity',
'/projects/apollo/wiki', '/projects/apollo/wiki/Wiki/edit', '/news/1', '/boards/1/topics/1', '/projects/apollo/documents',
'/projects/apollo/files', '/projects/apollo/time_entries', '/projects/apollo/time_entries/report?criteria[]=user&columns=month',
'/search?q=login', '/my/account', '/admin', '/settings', '/users', '/roles/permissions',
'/workflows/edit?role_id=3&tracker_id=1', '/workflows/permissions?role_id=3&tracker_id=1',
'/projects/apollo/repository', '/projects/apollo/repository/portal/revisions', '/projects/apollo/repository/portal/entry/lib/portal.rb',
'/projects/apollo/repository/portal/annotate/lib/portal.rb', '/projects/apollo/repository/portal/revisions/main/diff',
'/projects/apollo/wiki/Wiki/history', '/projects/apollo/wiki/Wiki/diff?version=2&version_from=1', '/projects/apollo/settings',
'/projects/apollo/settings/members', '/projects/apollo/settings/versions', '/projects/apollo/settings/repositories', '/trackers',
'/issue_statuses', '/custom_fields', '/custom_fields/1/edit', '/enumerations', '/admin/info', '/admin/plugins',
'/projects/apollo/boards',
];
// Elements whose core shape is intended. Each entry needs a reason.
const ALLOW = [
'div.gantt_hdr', // Gantt chart header cells form the chart grid
];
const viewports = {
desktop: { viewport: { width: 1440, height: 900 } },
mobile: { viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true },
};
const browser = await chromium.launch();
const offenders = new Map(); // "viewport selector reason" -> pages
for (const [vpName, opts] of Object.entries(viewports)) {
const page = await browser.newPage(opts);
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'));
for (const path of pages) {
await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' });
const found = await page.evaluate((allow) => {
const describe = (el) => {
const parts = [];
for (let e = el; e && e !== document.body && parts.length < 3; e = e.parentElement) {
parts.unshift(e.id ? `#${e.id}` : e.tagName.toLowerCase() + [...e.classList].slice(0, 2).map((c) => `.${c}`).join(''));
if (e.id) break;
}
return parts.join(' > ');
};
const opaque = (color) => !/^rgba\(.*,\s*0\)$/.test(color) && color !== 'transparent';
const sides = ['Top', 'Right', 'Bottom', 'Left'];
const out = [];
for (const el of document.querySelectorAll('body *:not(script):not(style):not(svg *)')) {
const r = el.getBoundingClientRect();
if (!r.width || !r.height || allow.some((s) => el.matches(s))) continue;
const cs = getComputedStyle(el);
if (cs.visibility === 'hidden' || cs.display === 'none') continue;
const borders = sides.filter((s) => parseFloat(cs[`border${s}Width`]) > 0 && cs[`border${s}Style`] !== 'none'
&& opaque(cs[`border${s}Color`]));
const radii = ['TopLeft', 'TopRight', 'BottomRight', 'BottomLeft'].map((c) => parseFloat(cs[`border${c}Radius`]));
if (radii.some((x) => x >= 1 && x <= 3) && (borders.length || opaque(cs.backgroundColor))) {
out.push([describe(el), `${Math.max(...radii)}px corner`]);
}
if (cs.boxShadow.includes('rgba(33, 37, 41')) out.push([describe(el), 'core shadow']);
if (borders.length === 4 && radii.every((x) => x === 0) && r.width >= 100 && r.height >= 32
&& !['TABLE', 'TD', 'TH', 'TR'].includes(el.tagName)) {
out.push([describe(el), 'square outlined box']);
}
const dashed = borders.find((s) => ['dashed', 'dotted'].includes(cs[`border${s}Style`]));
if (dashed) out.push([describe(el), `${cs[`border${dashed}Style`]} border`]);
}
return out;
}, ALLOW);
for (const [sel, reason] of found) {
const key = `${vpName.padEnd(7)} ${sel} (${reason})`;
if (!offenders.has(key)) offenders.set(key, new Set());
offenders.get(key).add(path);
}
}
await page.close();
}
await browser.close();
const rows = [...offenders].sort(([a], [b]) => a.localeCompare(b));
for (const [key, where] of rows) {
console.log(`${key} ${process.argv.includes('--verbose') ? [...where].join(', ') : `${where.size} page${where.size > 1 ? 's' : ''}`}`);
}
console.log(rows.length ? `\n${rows.length} core shapes left` : 'No core shapes left.');
process.exit(rows.length ? 1 : 0);