Files
Enelix-EMS/tools/public-docs/browser-test.mjs
T
2026-10-06 14:32:05 +00:00

144 lines
10 KiB
JavaScript

import { chromium } from 'playwright';
import fs from 'node:fs/promises';
import path from 'node:path';
import http from 'node:http';
import assert from 'node:assert/strict';
import { fileURLToPath } from 'node:url';
const root = path.dirname(fileURLToPath(import.meta.url));
await fs.mkdir(path.join(root, 'qa'), { recursive:true });
const catalogResponse = await fetch('https://license.enelix.ch/api/catalog');
assert.equal(catalogResponse.status, 200);
const fixture = await catalogResponse.json();
const live = process.argv.includes('--live');
const server = http.createServer(async (req, res) => {
const pathname = new URL(req.url, 'http://localhost').pathname;
if (pathname === '/api/catalog') { res.writeHead(200, { 'content-type':'application/json' }); return res.end(JSON.stringify(fixture)); }
const file = path.join(root, 'preview', pathname);
if (!file.startsWith(path.join(root, 'preview') + '/')) { res.writeHead(404); return res.end(); }
try {
const content = await fs.readFile(file);
const type = { '.html':'text/html', '.js':'text/javascript', '.css':'text/css', '.json':'application/json', '.png':'image/png' }[path.extname(file)] || 'application/octet-stream';
res.writeHead(200, { 'content-type':type }); res.end(content);
} catch { res.writeHead(404); res.end(); }
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const origin = live ? 'https://license.enelix.ch' : `http://127.0.0.1:${server.address().port}`;
process.env.TMPDIR = path.join(root, 'qa/runtime');
await fs.mkdir(process.env.TMPDIR, { recursive:true });
const standalone = (await import('@sparticuz/chromium')).default;
const browser = await chromium.launch({ headless:true, args:standalone.args.filter(arg => !['--single-process', '--disable-web-security', '--allow-running-insecure-content', '--disable-site-isolation-trials'].includes(arg) && !arg.startsWith('--disable-features=')), executablePath:await standalone.executablePath() });
const errors = [];
try {
for (const [label,width,height] of [['desktop',1440,1000], ['mobile',390,844], ['small-mobile',320,740]]) {
const context = await browser.newContext({ viewport:{width,height} });
const page = await context.newPage();
await page.clock.install();
page.on('pageerror', error => errors.push(error.message));
await page.goto(`${origin}/docs/index.html`);
assert.match(await page.locator('article h1').textContent(), /Enelix verstehen/);
assert.match(await page.locator('article').textContent(), /Energiemanagementsystem/);
await page.screenshot({ path:path.join(root, `qa/docs-${label}.png`) });
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, `${label} overview overflow`);
if (width < 720) { assert.equal(await page.locator('#docs-menu').getAttribute('open'), null); await page.locator('#docs-menu > summary').click(); }
const consumers = page.locator('[data-folder="verbraucher"]');
const interfaces = page.locator('[data-folder="schnittstellen"]');
const folderToggle = consumers.locator(':scope > summary');
assert.equal(await consumers.getAttribute('open'), null);
assert.equal(await consumers.locator('a').count(), 8);
assert.equal(await page.locator('nav[aria-label="Dokumentation"] a[href="/docs/ems/Manager.html"]:visible').count(), 1);
await folderToggle.focus(); await folderToggle.press('Enter');
assert.equal(await consumers.evaluate(el => el.open), true);
await page.screenshot({ path:path.join(root, `qa/navigation-${label}.png`) });
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, `${label} expanded navigation overflow`);
await folderToggle.press('Space');
assert.equal(await consumers.evaluate(el => el.open), false);
await page.locator('#doc-search').fill('Waermepumpe');
assert.equal(await consumers.evaluate(el => el.open), true);
assert.equal(await consumers.locator('a[href="/docs/ems/Waermepumpe.html"]:visible').count(), 1);
assert.equal(await interfaces.isVisible(), false);
await page.locator('#doc-search').fill('Schnittstellen');
assert.equal(await consumers.isVisible(), false);
assert.equal(await interfaces.evaluate(el => el.open), true);
assert.equal(await interfaces.locator('[data-search]:visible').count(), 3);
await page.locator('#doc-search').fill('');
assert.equal(await consumers.evaluate(el => el.open), false);
assert.equal(await interfaces.evaluate(el => el.open), false);
await folderToggle.click();
await page.locator('#doc-search').fill('Shelly');
await page.locator('#doc-search').fill('');
assert.equal(await consumers.evaluate(el => el.open), true, 'Search preserves manually expanded folders');
await page.locator('#doc-search').fill('Shelly');
assert.match(await page.locator('#search-result').textContent(), /passende Themen/);
assert.ok(await page.locator('[data-search]:visible').count() >= 1);
await page.locator('#doc-search').fill('unfindable-text-xxx');
assert.match(await page.locator('#search-result').textContent(), /Keine/);
assert.equal(await page.locator('#docs-menu section:visible').count(), 0);
await page.locator('#doc-search').fill('');
await consumers.locator('a[href="/docs/ems/Waermepumpe.html"]').click();
await page.waitForURL('**/docs/ems/Waermepumpe.html');
assert.equal(await consumers.evaluate(el => el.open), true, 'Current page opens its folder');
assert.equal(await consumers.locator('a[aria-current="page"]').getAttribute('href'), '/docs/ems/Waermepumpe.html');
for (const name of ['index','erste-schritte','manager']) {
await page.goto(`${origin}/docs/${name}.html`);
const images = page.locator('article img');
assert.ok(await images.count() > 0, `${name}: missing screenshot`);
for (const img of await images.all()) {
await img.scrollIntoViewIfNeeded();
await img.evaluate(el => el.decode());
assert.ok(await img.evaluate(el => el.naturalWidth >= 100 && el.naturalHeight >= 100));
assert.match(await img.getAttribute('src'), /^\/docs\/images\/symcon-[a-z-]+-[a-f0-9]{16}\.png$/);
assert.ok(await img.getAttribute('alt'));
}
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, `${label} ${name} image overflow`);
await page.screenshot({ path:path.join(root, `qa/${name}-image-${label}.png`) });
}
await page.goto(`${origin}/docs/preise.html`);
await page.waitForSelector('.price-table');
assert.equal(await page.locator('.price-table tbody tr').count(), Object.keys(fixture.items).length);
assert.ok((await page.locator('.price-table tbody tr').first().textContent()).includes((fixture.items.manager_standard.unitAmount / 100).toFixed(2)));
await page.locator('[value="gross"]').check();
const expectedGross = ((fixture.items.manager_standard.unitAmount + Math.round(fixture.items.manager_standard.unitAmount * fixture.vatRate / 100)) / 100).toFixed(2);
assert.ok((await page.locator('.price-table tbody tr').first().textContent()).includes(expectedGross));
await page.screenshot({ path:path.join(root, `qa/prices-${label}.png`) });
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, `${label} prices overflow`);
const changed = structuredClone(fixture); changed.items.manager_standard.unitAmount = 50000;
changed.items.new_test = { sku:'NEW', name:'Backend-Neuprodukt', unitAmount:1234, setupAmount:0, termMonths:6 };
await page.route('**/api/catalog', route => route.fulfill({ json:changed }));
await page.clock.fastForward(31000);
await page.waitForFunction(count => document.querySelectorAll('.price-table tbody tr').length === count, Object.keys(changed.items).length);
assert.ok((await page.locator('.price-table tbody tr').first().textContent()).includes(((50000 + Math.round(50000 * fixture.vatRate / 100)) / 100).toFixed(2)));
await page.unroute('**/api/catalog');
await page.route('**/api/catalog', route => route.fulfill({ status:503, body:'unavailable' }));
await page.locator('#refresh-prices').click();
await page.waitForSelector('#price-status.error');
assert.equal(await page.locator('.price-table').count(), 0);
await page.unroute('**/api/catalog');
await page.locator('#refresh-prices').click(); await page.waitForSelector('.price-table');
await page.goto(`${origin}/docs/ems/Manager.html`);
await page.screenshot({ path:path.join(root, `qa/manager-${label}.png`) });
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, `${label} reference overflow`);
const noJs = await browser.newContext({ javaScriptEnabled:false, viewport:{width,height} });
const staticPage = await noJs.newPage(); await staticPage.goto(`${origin}/docs/installation.html`);
assert.match(await staticPage.locator('article').textContent(), /Bibliotheken hinzufügen/);
const staticFolder = staticPage.locator('[data-folder="verbraucher"]');
await staticFolder.locator(':scope > summary').click();
assert.equal(await staticFolder.evaluate(el => el.open), true);
await staticFolder.locator('a[href="/docs/ems/Batterie.html"]').click();
await staticPage.waitForURL('**/docs/ems/Batterie.html');
assert.equal(await staticFolder.evaluate(el => el.open), true);
assert.equal(await staticFolder.locator('a[aria-current="page"]').getAttribute('href'), '/docs/ems/Batterie.html');
await noJs.close();
if (live) {
await page.goto(origin + '/');
await page.waitForSelector('#loginForm:visible');
assert.equal(await page.locator('.public-doc-links a').count(), 3);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true, `${label} login overflow`);
await page.screenshot({ path:path.join(root, `qa/portal-${label}.png`) });
}
await context.close();
console.log(`${label}: folders, keyboard navigation, search expansion/reset, active branch, screenshots, prices, API recovery, layout and no-JS passed`);
}
assert.deepEqual(errors, []);
} finally { await browser.close(); await new Promise(resolve => server.close(resolve)); }