import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import { createRequire } from 'node:module'; import { publicWizardCatalog, normalizeSystemConfiguration, recommendLicenses } from '../portal-domain.mjs'; import { publicCatalog } from '../stripe.mjs'; const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); const require = createRequire(process.env.BROWSER_PACKAGE || import.meta.url); const { chromium } = require('playwright'); const options = { headless: true }; if (process.env.BROWSER_EXECUTABLE) { options.executablePath = process.env.BROWSER_EXECUTABLE; const loaded = require('@sparticuz/chromium'); const standalone = loaded.default || loaded; options.args = standalone.args.filter(arg => !['--single-process', '--disable-web-security', '--allow-running-insecure-content', '--disable-site-isolation-trials'].includes(arg) && !arg.startsWith('--disable-features=')); } const browser = await chromium.launch(options); let plants = []; let submitted; const errors = []; const json = (route, value) => route.fulfill({ contentType: 'application/json', body: JSON.stringify(value) }); try { const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, locale: 'de-CH' }); page.on('pageerror', error => errors.push(error.message)); await page.route('**/*', async route => { const url = new URL(route.request().url()); if (url.pathname === '/api/catalog') return json(route, { items: publicCatalog(), vatRate: 8.1 }); if (url.pathname === '/api/wizard-catalog') return json(route, publicWizardCatalog()); if (url.pathname === '/api/session') return json(route, { authenticated: true, user: { name: 'Testkonto', email: 'portal-test@example.invalid' }, organization: { type: 'personal', role: 'owner', name: 'Testbereich' }, organizations: [] }); if (url.pathname === '/healthz') return json(route, { stripe: 'not_configured' }); if (url.pathname === '/api/plants') return json(route, { plants }); if (url.pathname === '/api/orders') return json(route, { orders: [] }); if (url.pathname === '/api/configurations') { submitted = normalizeSystemConfiguration(route.request().postDataJSON()); const recommendation = recommendLicenses(submitted); plants = [{ id: 'test-plant', name: submitted.name, location: submitted.location, type: submitted.siteType, manager_variant: 'manager_standard', configuration_id: 'test-config', configuration_json: JSON.stringify(submitted), recommendation_json: JSON.stringify(recommendation) }]; return json(route, { plant: plants[0], configurationId: 'test-config', packageReady: false, purchaseProposal: { required: true } }); } const file = url.pathname === '/' ? 'index.html' : url.pathname.slice(1); if (!['index.html', 'styles.css', 'docs-portal.css', 'app.js'].includes(file)) return route.fulfill({ status: 404, body: 'Not found' }); return route.fulfill({ contentType: file.endsWith('.js') ? 'text/javascript' : file.endsWith('.css') ? 'text/css' : 'text/html', body: await fs.readFile(path.join(root, 'public', file)) }); }); await page.goto('http://portal.test/'); await page.locator('#portalApp:not(.hidden)').waitFor(); for (const width of [1440, 768, 390, 320]) { await page.setViewportSize({ width, height: 1000 }); const layout = await page.locator('.topbar').evaluate(element => { const nodes = [...element.querySelectorAll('.brand, .header-links a, .account-button')].map(node => { const box = node.getBoundingClientRect(); return { x: box.x, right: box.right, top: box.top, bottom: box.bottom }; }); return { nodes, viewport: innerWidth, width: document.documentElement.scrollWidth }; }); assert.ok(layout.nodes.every(box => box.x >= 0 && box.right <= width), JSON.stringify({ width, layout })); for (let i = 1; i < layout.nodes.length; i++) assert.ok(layout.nodes[i - 1].right <= layout.nodes[i].x, JSON.stringify({ width, layout })); assert.equal(await page.locator('.sidebar a[href^="/docs/"]').count(), 0); await page.screenshot({ path: path.join(root, `header-${width}.png`), clip: { x: 0, y: 0, width, height: 180 } }); } await page.setViewportSize({ width: 1440, height: 1000 }); await page.locator('[data-open-new-license]').first().click(); await page.locator('#newLicenseDialog [data-goto="configurator"]').click(); await page.locator('#systemForm [name="name"]').fill('Testanlage'); await page.locator('#systemForm [name="location"]').fill('Teststandort'); for (let i = 0; i < 7; i++) await page.locator('#wizardNext').click(); assert.equal(await page.locator('#wizardStepTitle').textContent(), 'Wärmepumpe'); assert.equal(await page.locator('.wizard-progress li').evaluateAll(nodes => nodes.every(node => node.scrollWidth <= node.clientWidth)), true); await page.locator('[name="heatPumpQuantity"]').fill('1'); const editor = page.locator('[data-device-editor="heat_pump"]'); await editor.locator('[data-device-property="Kontaktart"]').selectOption('1'); for (const [field, value] of Object.entries({ Kontakt1VariableID: 10001, Kontakt2VariableID: 10002, IstleistungVariableID: 10003, Nennleistung: 4200, Mindestlaufzeit: 1500 })) { await editor.locator(`[data-device-property="${field}"]`).fill(String(value)); } await editor.locator('[data-device-property="Rueckmeldungsart"]').selectOption('1'); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: path.join(root, 'wizard-desktop.png'), fullPage: true }); await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: path.join(root, 'wizard-mobile.png'), fullPage: true }); await page.locator('#wizardNext').click(); await page.locator('#wizardNext').click(); assert.match(await page.locator('#wizardReview').textContent(), /Wärmepumpe/); await page.locator('#wizardSubmit').click(); await page.locator('#view-licenses.active').waitFor(); assert.equal(submitted.devices.heat_pump.items[0].properties.Mindestlaufzeit, 1500); assert.equal(submitted.devices.heat_pump.items[0].properties.Kontaktart, 1); assert.equal(await page.locator('.module-row[data-key="heat_pump"] .module-check').isChecked(), true); assert.match(await page.locator('#orderItems').textContent(), /Wärmepumpe/); await page.locator('[data-configure-current]').click(); for (let i = 0; i < 7; i++) await page.locator('#wizardNext').click(); assert.equal(await editor.locator('[data-device-property="Kontakt1VariableID"]').inputValue(), '10001'); assert.equal(await editor.locator('[data-device-property="Mindestlaufzeit"]').inputValue(), '1500'); assert.deepEqual(errors, []); console.log(JSON.stringify({ ok: true, headerWidths: [1440, 768, 390, 320], wizard: 'configure, save, license proposal, reopen', browserErrors: errors })); } finally { await browser.close(); }