Initial commit: Tonalmine, a Material Design 3 theme for Redmine 7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// 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}`);
|
||||
Reference in New Issue
Block a user