Files
surtic86andClaude Opus 5.5 232958cd13 test: SIZE option for screenshots and form alignment; scale filter rows (T9)
Query filter rows kept a fixed 40px height, so in Compact their labels
sat 3px below the operator select.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:24:04 +02:00

151 lines
6.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Captures the page checklist in light/dark × desktop/mobile into ../screenshots/.
// Usage: node screenshots.mjs [filter] e.g. `node screenshots.mjs issue` for pages whose name contains "issue".
// SIZE=compact|large node screenshots.mjs (in that size mode, files end in -compact/-large; needs the plugin)
// Needs the dev server with seed data (see README).
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';
import { setSize } from './size.mjs';
const BASE = process.env.REDMINE_URL ?? 'http://127.0.0.1:3070';
const OUT = new URL('../screenshots/', import.meta.url).pathname;
const filter = process.argv[2] ?? '';
const SIZE = process.env.SIZE;
const pages = [
['my-page', '/my/page'],
['projects-board', '/projects?display_type=board'],
['projects-list', '/projects?display_type=list'],
['project-overview', '/projects/apollo'],
['issues-grouped', '/projects/apollo/issues?set_filter=1&f[]=status_id&op[status_id]=*&group_by=status&t[]=estimated_hours&t[]=spent_hours'],
['issue-show', '/issues/13'],
['issue-new', '/projects/apollo/issues/new'],
['issue-edit', '/issues/13/edit'],
['issue-bulk-edit', '/issues/bulk_edit?ids[]=13&ids[]=17'],
['gantt', '/projects/apollo/issues/gantt'],
['calendar', '/projects/apollo/issues/calendar'],
['roadmap', '/projects/apollo/roadmap'],
['version', '/versions/2'],
['activity', '/projects/apollo/activity'],
['wiki', '/projects/apollo/wiki'],
['wiki-edit', '/projects/apollo/wiki/Wiki/edit'],
['news', '/news/1'],
['forum-topic', '/boards/1/topics/1'],
['documents', '/projects/apollo/documents'],
['files', '/projects/apollo/files'],
['time-entries', '/projects/apollo/time_entries'],
['time-report', '/projects/apollo/time_entries/report?criteria[]=user&columns=month'],
['search', '/search?q=login'],
['my-account', '/my/account'],
['admin', '/admin'],
['settings', '/settings'],
['users', '/users'],
['permissions', '/roles/permissions'],
['workflow', '/workflows/edit?role_id=3&tracker_id=1'],
['not-found', '/issues/99999'],
['repository', '/projects/apollo/repository'],
['repository-revisions', '/projects/apollo/repository/portal/revisions'],
['repository-entry', '/projects/apollo/repository/portal/entry/lib/portal.rb'],
['repository-annotate', '/projects/apollo/repository/portal/annotate/lib/portal.rb'],
['repository-diff', '/projects/apollo/repository/portal/revisions/main/diff'],
['wiki-history', '/projects/apollo/wiki/Wiki/history'],
['wiki-diff', '/projects/apollo/wiki/Wiki/diff?version=2&version_from=1'],
['project-settings', '/projects/apollo/settings'],
['project-settings-members', '/projects/apollo/settings/members'],
['project-settings-versions', '/projects/apollo/settings/versions'],
['project-settings-repositories', '/projects/apollo/settings/repositories'],
['trackers', '/trackers'],
['issue-statuses', '/issue_statuses'],
['custom-fields', '/custom_fields'],
['custom-field-edit', '/custom_fields/1/edit'],
['enumerations', '/enumerations'],
['admin-info', '/admin/info'],
['admin-plugins', '/admin/plugins'],
['workflow-fields', '/workflows/permissions?role_id=3&tracker_id=1'],
['forums', '/projects/apollo/boards'],
];
// Pages only reachable while logged out.
const publicPages = [
['login', '/login'],
['register', '/account/register'],
['lost-password', '/account/lost_password'],
];
// Pages that need an interaction before the capture.
const interactions = [
['issue-new-error', '/projects/apollo/issues/new', async (p) => {
await p.click('#issue-form input[name=commit]');
await p.waitForLoadState('networkidle');
}],
['issues-context-menu', '/projects/apollo/issues', async (p) => {
await p.click('table.list.issues tr.issue td.status', { button: 'right' });
await p.waitForSelector('#context-menu ul');
}, { desktopOnly: true }],
['journal-diff', '/issues/13', async (p) => {
// The journal ID differs per database, so follow the first description diff link.
await Promise.all([p.waitForURL(/\/journals\/\d+\/diff/), p.click('#history a[href*="/diff"]')]);
await p.waitForLoadState('networkidle');
}],
['import-mapping', '/issues/imports/new?project_id=apollo', async (p) => {
await p.setInputFiles('#file', new URL('fixtures/issues.csv', import.meta.url).pathname);
await Promise.all([p.waitForURL(/\/imports\/[^/]+\/settings/), p.click('input[type=submit]')]);
await Promise.all([p.waitForURL(/\/imports\/[^/]+\/mapping/), p.click('input[type=submit]')]);
await p.waitForLoadState('networkidle');
}],
['project-jump', '/projects/apollo-api/issues', async (p) => {
await p.click('#project-jump .drdn-trigger');
await p.waitForSelector('#project-jump .drdn-items');
}, { desktopOnly: true }],
['mobile-flyout', '/projects/apollo/issues', async (p) => {
await p.click('.mobile-toggle-button');
await p.waitForTimeout(300);
}, { mobileOnly: true, viewportOnly: true }],
];
const viewports = {
desktop: { width: 1440, height: 900 },
mobile: { width: 390, height: 844, isMobile: true, hasTouch: true },
};
await mkdir(OUT, { recursive: true });
const browser = await chromium.launch();
for (const scheme of ['light', 'dark']) {
for (const [vpName, vp] of Object.entries(viewports)) {
const { width, height, ...rest } = vp;
const context = await browser.newContext({ viewport: { width, height }, colorScheme: scheme, ...rest });
// Logged out, the size comes from localStorage; logged in, from the account (set below)
if (SIZE) await context.addInitScript((size) => localStorage.setItem('tonalmine', JSON.stringify({ size })), SIZE);
const page = await context.newPage();
const shot = (name, fullPage = true) =>
page.screenshot({ path: `${OUT}${name}-${scheme}-${vpName}${SIZE ? `-${SIZE}` : ''}.png`, fullPage });
for (const [name, path] of publicPages.filter(([n]) => n.includes(filter))) {
await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' });
await shot(name);
}
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'));
if (SIZE) await setSize(page, SIZE);
for (const [name, path] of pages.filter(([n]) => n.includes(filter))) {
await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' });
await shot(name);
}
for (const [name, path, act, opts = {}] of interactions.filter(([n]) => n.includes(filter))) {
if ((opts.desktopOnly && vpName !== 'desktop') || (opts.mobileOnly && vpName !== 'mobile')) continue;
await page.goto(`${BASE}${path}`, { waitUntil: 'networkidle' });
await act(page);
await shot(name, !opts.viewportOnly);
}
if (SIZE) await setSize(page);
await context.close();
}
}
await browser.close();
console.log(`Screenshots written to ${OUT}`);