docs: plain README screenshots, diagonal only for the title and a colour-family grid
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m33s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m33s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2f795c7a7
commit
e045f0edd4
15 files changed
+67
-28
No files matched your search
+47
-22
@@ -1,6 +1,7 @@
|
||||
// Takes the README screenshots from a fresh server in demo mode (sample data of admin/admin).
|
||||
// Every picture is taken twice in the same state – light and dark theme – and the two halves are
|
||||
// joined along a slanted diagonal (light left, dark right).
|
||||
// The title picture and the colour-family grid are taken twice in the same state – light and dark
|
||||
// theme – and joined along a slanted diagonal (light left, dark right); the others are plain light
|
||||
// or dark pictures.
|
||||
// Usage: e2e/screenshots.sh (builds the image) or MINDSIFT_URL=… SHOTS_DIR=… node screenshots.mjs
|
||||
// Theme pair: SHOT_LIGHT / SHOT_DARK (default: light / dark, the "Klassik" family)
|
||||
import fs from 'node:fs';
|
||||
@@ -13,6 +14,13 @@ const DARK = process.env.SHOT_DARK || 'dark';
|
||||
const DESKTOP = { viewport: { width: 1518, height: 886 }, deviceScaleFactor: 2 };
|
||||
const MOBILE = { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
// Colour families for the theme grid in the README (Klassik is the title picture)
|
||||
const FAMILIES = [
|
||||
['fjord', 'fjord', 'fjord-night'],
|
||||
['terracotta', 'terracotta', 'terracotta-night'],
|
||||
['polarized', 'polarized-light', 'polarized'],
|
||||
['suri', 'suri-light', 'suri'],
|
||||
];
|
||||
|
||||
async function login(user) {
|
||||
const res = await fetch(`${BASE}/api/v1/auth/login`, {
|
||||
@@ -57,10 +65,11 @@ async function setTheme(page, id, dark) {
|
||||
}
|
||||
|
||||
let composer;
|
||||
// Light picture on the left of the diagonal, dark on the right, a thin line between them
|
||||
async function compose(lightPng, darkPng, file) {
|
||||
// Light picture on the left of the diagonal, dark on the right, a thin line between them;
|
||||
// scale < 1 shrinks the result (small pictures of the theme grid)
|
||||
async function compose(lightPng, darkPng, file, scale = 1) {
|
||||
const dataUrl = await composer.evaluate(
|
||||
async ([light, dark]) => {
|
||||
async ([light, dark, scale]) => {
|
||||
const load = (b64) =>
|
||||
new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
@@ -68,13 +77,14 @@ async function compose(lightPng, darkPng, file) {
|
||||
img.src = 'data:image/png;base64,' + b64;
|
||||
});
|
||||
const [a, b] = await Promise.all([load(light), load(dark)]);
|
||||
const w = a.naturalWidth;
|
||||
const h = a.naturalHeight;
|
||||
const w = Math.round(a.naturalWidth * scale);
|
||||
const h = Math.round(a.naturalHeight * scale);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(b, 0, 0);
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(b, 0, 0, w, h);
|
||||
const top = w * 0.58;
|
||||
const bottom = w * 0.42;
|
||||
ctx.save();
|
||||
@@ -85,7 +95,7 @@ async function compose(lightPng, darkPng, file) {
|
||||
ctx.lineTo(0, h);
|
||||
ctx.closePath();
|
||||
ctx.clip();
|
||||
ctx.drawImage(a, 0, 0);
|
||||
ctx.drawImage(a, 0, 0, w, h);
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
|
||||
ctx.lineWidth = Math.max(2, w / 600);
|
||||
@@ -97,19 +107,30 @@ async function compose(lightPng, darkPng, file) {
|
||||
ctx.stroke();
|
||||
return canvas.toDataURL('image/png');
|
||||
},
|
||||
[lightPng.toString('base64'), darkPng.toString('base64')]
|
||||
[lightPng.toString('base64'), darkPng.toString('base64'), scale]
|
||||
);
|
||||
fs.writeFileSync(`${OUT}/${file}`, Buffer.from(dataUrl.split(',')[1], 'base64'));
|
||||
}
|
||||
|
||||
async function shot(page, file) {
|
||||
// Diagonal light/dark picture
|
||||
async function shot(page, file, { light = LIGHT, dark = DARK, scale = 1 } = {}) {
|
||||
await sleep(800); // animations
|
||||
await page.mouse.move(0, 0);
|
||||
await setTheme(page, LIGHT, false);
|
||||
const light = await page.screenshot();
|
||||
await setTheme(page, DARK, true);
|
||||
const dark = await page.screenshot();
|
||||
await compose(light, dark, file);
|
||||
await setTheme(page, light, false);
|
||||
const lightPng = await page.screenshot();
|
||||
await setTheme(page, dark, true);
|
||||
const darkPng = await page.screenshot();
|
||||
await compose(lightPng, darkPng, file, scale);
|
||||
console.log(`OK ${file}`);
|
||||
}
|
||||
|
||||
// Plain picture in one theme ('light' or 'dark' of the chosen pair)
|
||||
async function plain(page, file, mode) {
|
||||
await sleep(800);
|
||||
await page.mouse.move(0, 0);
|
||||
if (mode === 'light') await setTheme(page, LIGHT, false);
|
||||
else await setTheme(page, DARK, true);
|
||||
await page.screenshot({ path: `${OUT}/${file}` });
|
||||
console.log(`OK ${file}`);
|
||||
}
|
||||
|
||||
@@ -126,7 +147,11 @@ const main = async () => {
|
||||
await tab(page, 'Aufgaben');
|
||||
await page.getByText('Homelab', { exact: true }).first().click().catch(() => {});
|
||||
await page.getByRole('button', { name: /Kanban|Board/ }).first().click().catch(() => {});
|
||||
await shot(page, 'aufgaben_desktop.png');
|
||||
await plain(page, 'aufgaben_desktop.png', 'dark');
|
||||
// Colour families: the board fills the width, so both halves of the diagonal show content
|
||||
for (const [family, light, dark] of FAMILIES) {
|
||||
await shot(page, `Theme_${family}.png`, { light, dark, scale: 0.5 });
|
||||
}
|
||||
|
||||
// The demo user edits a bullet of the shared note, so the screenshot shows lock and presence
|
||||
const other = await open(browser, user, DESKTOP);
|
||||
@@ -142,7 +167,7 @@ const main = async () => {
|
||||
await page.waitForFunction(() => document.querySelectorAll('textarea').length > 5, null, { timeout: 10000 });
|
||||
await page.getByText('User', { exact: false }).first().waitFor({ timeout: 15000 }).catch(() => {});
|
||||
await sleep(2000);
|
||||
await shot(page, 'workspace_desktop.png');
|
||||
await plain(page, 'workspace_desktop.png', 'light');
|
||||
|
||||
// Journal: a few days with entries, the calendar in the sidebar
|
||||
const H = { Authorization: `Bearer ${admin}`, 'Content-Type': 'application/json' };
|
||||
@@ -171,22 +196,22 @@ const main = async () => {
|
||||
await page.getByRole('button', { name: 'Heute' }).click().catch(() => {});
|
||||
await page.mouse.click(1000, 860);
|
||||
await sleep(2500);
|
||||
await shot(page, 'journal_desktop.png');
|
||||
await plain(page, 'journal_desktop.png', 'dark');
|
||||
await page.getByRole('button', { name: 'Journal', exact: true }).click(); // close it again
|
||||
|
||||
await tab(page, 'Listen');
|
||||
await page.getByText('Wocheneinkauf', { exact: true }).first().click().catch(() => {});
|
||||
await shot(page, 'Listen_desktop.png');
|
||||
await plain(page, 'Listen_desktop.png', 'light');
|
||||
|
||||
const mobile = await open(browser, admin, MOBILE);
|
||||
await mobile.getByText('Später im Browser weiter nutzen').click({ timeout: 5000 }).catch(() => {}); // install prompt
|
||||
await shot(mobile, 'Mobile_Triage.png');
|
||||
await plain(mobile, 'Mobile_Triage.png', 'light');
|
||||
|
||||
await tab(mobile, 'Workspace');
|
||||
await sleep(600);
|
||||
await mobile.getByText(/^Homelab 2\.0/).first().click();
|
||||
await sleep(1500);
|
||||
await shot(mobile, 'Mobile_Workspace.png');
|
||||
await plain(mobile, 'Mobile_Workspace.png', 'dark');
|
||||
|
||||
await browser.close();
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user