Files
redmine-theme-tonalmine/e2e/screenshots.mjs
T

144 lines
6.3 KiB
JavaScript
Raw 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".
// Needs the dev server with seed data (see README).
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';
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 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 });
const page = await context.newPage();
const shot = (name, fullPage = true) =>
page.screenshot({ path: `${OUT}${name}-${scheme}-${vpName}.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'));
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);
}
await context.close();
}
}
await browser.close();
console.log(`Screenshots written to ${OUT}`);