Files
Yuvomi-App/scripts/capture-pwa-reference.mjs
T
Yuvomi Mobile Contributors 7ac9882307
Android validation / emulator (37.0) (push) Failing after 3s
Android validation / emulator (35) (push) Failing after 4s
iOS validation / simulator (push) Successful in 59s
Android validation / build (push) Successful in 5m30s
feat(android): align native preview with measured PWA design
2026-10-21 10:45:16 +02:00

106 lines
6.3 KiB
JavaScript

// Browser automation is a reference-measurement tool, never an application renderer.
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const [playwrightModule, destination] = process.argv.slice(2);
const require = createRequire(import.meta.url);
const { chromium } = require(playwrightModule);
const origin = process.env.YUVOMI_TEST_URL;
if (!/^https:\/\/127\.0\.0\.1:\d+$/.test(origin)) throw new Error('Only the temporary loopback fixture is allowed');
const browser = await chromium.launch({ headless: true, args: process.getuid?.() === 0 ? ['--no-sandbox'] : [] });
const width = Number(process.env.YUVOMI_VISUAL_WIDTH || 360);
const height = Number(process.env.YUVOMI_VISUAL_HEIGHT || 640);
const measurements = {
upstreamRevision: '10bfef7686ed7303b4c74c7fcc55a5eb16356c9d', browser: browser.version(),
androidFontConfiguration: Boolean(process.env.FONTCONFIG_FILE),
fixture: { widgets: ['tasks', 'calendar', 'shopping', 'notes'], mobileFavorites: ['tasks', 'kitchen', 'notes'] }, screens: [],
};
try {
const context = await browser.newContext({
viewport: { width, height }, deviceScaleFactor: 2, isMobile: true, hasTouch: true, reducedMotion: 'reduce',
locale: 'de-DE', timezoneId: 'Europe/Berlin', ignoreHTTPSErrors: true,
userAgent: 'Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36',
serviceWorkers: 'block',
});
await context.route('**/*', route => new URL(route.request().url()).origin === origin ? route.continue() : route.abort());
const api = context.request;
const password = process.env.YUVOMI_TEST_PASSWORD;
let csrf = '';
async function request(route, data) {
const response = await api.fetch(origin + '/api/v1' + route, {
method: data === undefined ? 'GET' : (route === '/preferences' ? 'PUT' : 'POST'),
data, headers: { 'X-CSRF-Token': csrf },
});
if (!response.ok()) throw new Error(`Fixture API failed at ${route}: HTTP ${response.status()}`);
const result = await response.json();
if (result.csrfToken) csrf = result.csrfToken;
return result;
}
await request('/auth/setup', { username: 'mobiletest', display_name: 'Mobile Test', password });
await request('/auth/login', { username: 'mobiletest', password });
await request('/auth/onboarding-seen', {});
await request('/auth/changelog-seen', {});
const today = new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Berlin' }).format(new Date());
await request('/tasks', { title: 'Gemeinsam den Alltag planen', priority: 'high', due_date: today });
await request('/notes', { title: 'Willkommen zuhause', content: 'Native Oberfläche, eigener Server.', pinned: 1 });
const list = await request('/shopping', { name: 'Wochenmarkt' });
await request(`/shopping/${list.data.id}/items`, { name: 'Äpfel', quantity: '4' });
// Match the implemented native slice, not a fabricated full-module acceptance.
await request('/preferences', {
dashboard_widgets: ['tasks', 'calendar', 'shopping', 'notes'].map((id, order) => ({ id, visible: true, order, size: '1x1' })),
dashboard_today_glance: false, mobile_nav_order: ['tasks', 'kitchen', 'notes'],
timezone: 'Europe/Berlin', timezone_hint_dismissed: true,
});
await request('/auth/logout', {});
const page = await context.newPage();
await page.addInitScript(() => {
localStorage.setItem('yuvomi-language', 'de');
localStorage.setItem('yuvomi-lang', 'de');
});
const selectors = ['.auth-page', '.auth-hero', '.auth-hero__mark', '.auth-hero__title', '.auth-hero__tagline', '.label', '.input', '.btn--primary', '.dashboard-overview__date',
'.dashboard-overview__title', '.widget', '.widget__header', '.widget__title', '.widget__body', '.nav-bottom__items',
'.nav-bottom .nav-item', '.page-toolbar', '.page-toolbar__title', '.task-card', '.task-card__title', '.note-card', '.list-row'];
async function capture(name) {
await page.evaluate(() => document.fonts.ready);
// Compare neutral states, not a keyboard focus ring left by fixture navigation.
await page.evaluate(() => document.activeElement?.blur?.());
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
await page.screenshot({ path: path.join(destination, `${name}.png`), animations: 'disabled' });
measurements.screens.push({ name, viewport: page.viewportSize(), elements: await page.evaluate(selectors =>
selectors.flatMap(selector => [...document.querySelectorAll(selector)].slice(0, 5).map(element => {
const bounds = element.getBoundingClientRect(); const style = getComputedStyle(element);
return { selector, text: element.matches('input') ? '' : element.textContent.trim().slice(0, 80),
x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height,
font: style.fontFamily, fontSize: style.fontSize, fontWeight: style.fontWeight, lineHeight: style.lineHeight,
letterSpacing: style.letterSpacing, color: style.color, background: style.backgroundColor,
radius: style.borderRadius, padding: style.padding };
})), selectors) });
}
for (const theme of ['light', 'dark']) {
await page.emulateMedia({ colorScheme: theme, reducedMotion: 'reduce' });
await page.goto(origin + '/login');
await page.evaluate(value => localStorage.setItem('yuvomi-theme', value), theme);
await page.reload();
await page.locator('#auth-btn').waitFor();
await capture(`login-${theme}-${width}`);
await page.locator('#username').fill('mobiletest');
await page.locator('#password').fill(password);
await page.locator('#auth-btn').click();
await page.locator('.dashboard-overview__title').waitFor();
await page.locator('.widget--tasks .widget__body').waitFor();
await capture(`dashboard-${theme}-${width}`);
for (const [route, selector, name] of [['/tasks', '.task-card', 'tasks'], ['/notes', '.note-card', 'notes'], ['/shopping', '.shopping-item', 'shopping']]) {
await page.goto(origin + route);
await page.locator(selector).first().waitFor();
await capture(`${name}-${theme}-${width}`);
}
await request('/auth/me');
await request('/auth/logout', {});
}
await context.close();
} finally {
fs.writeFileSync(path.join(destination, 'measurements.json'), JSON.stringify(measurements, null, 2) + '\n');
await browser.close();
}