feat(workflow): publish verified forecast controls and consolidated docs
Tests / test (push) Successful in 1m1s
Tests / test (push) Successful in 1m1s
Approved by Daniel Haefliger for develop and beta. Author dh_Agent, authenticated account dh. Preserve published battery, charging and overall Energy Pie changes. No deployment or plant control authorization.
This commit is contained in:
@@ -4,7 +4,20 @@ import { createServer } from 'node:http';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { createRequire } from 'node:module';
|
||||
import { join } from 'node:path';
|
||||
const { chromium } = createRequire(import.meta.url)('playwright');
|
||||
const require = createRequire(process.env.BROWSER_PACKAGE || import.meta.url);
|
||||
const { chromium } = require('playwright');
|
||||
const browserOptions = { headless: true };
|
||||
if (process.env.BROWSER_EXECUTABLE) {
|
||||
browserOptions.executablePath = process.env.BROWSER_EXECUTABLE;
|
||||
const loaded = require('@sparticuz/chromium');
|
||||
const standalone = loaded.default || loaded;
|
||||
browserOptions.args = standalone.args.filter(arg => ![
|
||||
'--single-process', '--disable-web-security', '--allow-running-insecure-content',
|
||||
'--disable-site-isolation-trials', '--no-sandbox', '--disable-setuid-sandbox'
|
||||
].includes(arg) && !arg.startsWith('--disable-features='));
|
||||
} else if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {
|
||||
browserOptions.executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
|
||||
}
|
||||
|
||||
const stamp = minute => new Date(Date.UTC(2026, 9, 6, 10, minute)).toISOString();
|
||||
const state = {
|
||||
@@ -40,7 +53,7 @@ test('embedded planner: responsive charts, keyboard/pointer inspection, toggles
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true, ...(process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ? { executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH } : {}) });
|
||||
browser = await chromium.launch(browserOptions);
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.route('**/api/**', async route => {
|
||||
@@ -52,6 +65,19 @@ test('embedded planner: responsive charts, keyboard/pointer inspection, toggles
|
||||
result.plan.inputQuality.loadBasis = 'house_total';
|
||||
result.plan.forecastPoints.forEach(point => { point.externalW = null; });
|
||||
}
|
||||
const scenario = new URL(request.url()).pathname.split('/')[3];
|
||||
if (['disabled', 'collecting', 'learning', 'ready', 'active', 'interrupted', 'expired', 'contradictory', 'no-plan', 'hostile-reason'].includes(scenario)) {
|
||||
const workflowState = scenario === 'expired' || scenario === 'contradictory' ? 'active' : scenario === 'no-plan' ? 'collecting' : scenario === 'hostile-reason' ? 'interrupted' : scenario;
|
||||
const now = Date.now();
|
||||
result.workflow = {
|
||||
state: workflowState, learningEnabled: scenario !== 'disabled', controlRequested: workflowState === 'active',
|
||||
controlActive: workflowState === 'active' && scenario !== 'contradictory', planningReady: ['ready', 'active'].includes(workflowState),
|
||||
reportFresh: true, checkedAt: new Date(now).toISOString(), sourceReportAt: new Date(now - 1000).toISOString(),
|
||||
sourceFreshUntil: new Date(now + (scenario === 'expired' ? -1 : 119000)).toISOString(),
|
||||
reason: scenario === 'hostile-reason' ? '<img src=x onerror="window.injected=true">' : scenario === 'collecting' ? 'collecting_measurements' : ''
|
||||
};
|
||||
if (scenario === 'no-plan') result.plan = null;
|
||||
}
|
||||
await route.fulfill({ contentType: 'application/json', body: JSON.stringify(request.method() === 'PUT' ? { revision: 3 } : result) });
|
||||
});
|
||||
await page.goto(`http://127.0.0.1:${server.address().port}/`);
|
||||
@@ -59,10 +85,15 @@ test('embedded planner: responsive charts, keyboard/pointer inspection, toggles
|
||||
assert.equal(await page.locator('.forecast-planner .forecast-chart').count(), 6);
|
||||
assert.equal(await page.locator('#legacy-chart').evaluate(node => getComputedStyle(node).height), '320px');
|
||||
assert.equal(await page.locator('#legacy-legend').evaluate(node => getComputedStyle(node).borderBottomWidth), '1px');
|
||||
await page.locator('#legacy-chart, #legacy-legend').evaluateAll(nodes => {
|
||||
for (const node of nodes) node.style.display = 'none';
|
||||
});
|
||||
assert.equal(await page.locator('select#plants').count(), 0);
|
||||
assert.equal(requests.length, 1);
|
||||
assert.match(await page.locator('#host').innerText(), /tatsaechliche Anlagenbetrieb wird hier nicht bestaetigt/);
|
||||
assert.match(await page.locator('#host').innerText(), /Vorhanden; lokale Schutzpruefung/);
|
||||
assert.equal(await page.locator('.forecast-workflow-state').innerText(), 'Betriebsstatus nicht bestaetigt');
|
||||
assert.match(await page.locator('#host').innerText(), /berechnete Plaene, keine nachgewiesene Ausfuehrung/);
|
||||
assert.equal(await page.locator('.forecast-planner details[open]').count(), 0);
|
||||
assert.deepEqual(await page.locator('.forecast-planner .forecast-chart h3').allTextContents(), ['PV- und Hausprognose', 'Strompreis Bezug und Einspeisung', 'Netzfahrplan', 'Batterieleistung', 'Ladezustand battery', 'Kumulierte Kosten inklusive vollem Peakpreis']);
|
||||
const chart = page.locator('.forecast-planner .forecast-chart').first();
|
||||
const slider = chart.locator('input[type=range]');
|
||||
assert.equal(await slider.getAttribute('max'), '23');
|
||||
@@ -74,6 +105,7 @@ test('embedded planner: responsive charts, keyboard/pointer inspection, toggles
|
||||
await chart.getByLabel('PV-Prognose', { exact: true }).uncheck();
|
||||
assert.equal(await chart.locator('dt').count(), 2);
|
||||
await chart.getByLabel('PV-Prognose', { exact: true }).check();
|
||||
assert.equal(requests.length, 1, 'chart inspection and visibility never change planner settings or controls');
|
||||
await chart.locator('svg').scrollIntoViewIfNeeded();
|
||||
const bounds = await chart.locator('svg').boundingBox();
|
||||
await page.mouse.move(bounds.x + bounds.width - 20, bounds.y + 70);
|
||||
@@ -85,13 +117,41 @@ test('embedded planner: responsive charts, keyboard/pointer inspection, toggles
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), true, `no horizontal overflow at ${width}`);
|
||||
assert.ok((await chart.boundingBox()).height > 320, 'legacy SVG height must not constrain new chart sections');
|
||||
assert.ok(await chart.locator('polyline').count() >= 3);
|
||||
if (process.env.FORECAST_SCREENSHOT_DIR) await chart.screenshot({ path: join(process.env.FORECAST_SCREENSHOT_DIR, `forecast-chart-${width}.png`) });
|
||||
if (process.env.FORECAST_SCREENSHOT_DIR) {
|
||||
await chart.screenshot({ path: join(process.env.FORECAST_SCREENSHOT_DIR, `forecast-chart-${width}.png`) });
|
||||
await page.locator('.forecast-workflow').screenshot({ path: join(process.env.FORECAST_SCREENSHOT_DIR, `forecast-workflow-${width}.png`) });
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await page.screenshot({ path: join(process.env.FORECAST_SCREENSHOT_DIR, `forecast-viewport-${width}.png`) });
|
||||
await page.screenshot({ path: join(process.env.FORECAST_SCREENSHOT_DIR, `forecast-full-${width}.png`), fullPage: true });
|
||||
}
|
||||
}
|
||||
await page.locator('summary').click();
|
||||
await page.getByText('Erweiterte Planungseinstellungen', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Speichern und neu berechnen', exact: true }).click();
|
||||
await page.getByRole('status').filter({ hasText: 'Revision 3 gespeichert' }).waitFor();
|
||||
assert.equal(requests.at(-1).csrf, 'test-only');
|
||||
assert.equal(requests.some(request => /trial|arm|live/.test(request.url)), false);
|
||||
const showPlant = async plantId => page.evaluate(async id => {
|
||||
window.planner.dispose();
|
||||
window.planner = window.mountPlanner(document.querySelector('#host'), { plantId: id, session: {} });
|
||||
await window.planner.ready;
|
||||
}, plantId);
|
||||
for (const [scenario, label] of Object.entries({ disabled: 'Ausgeschaltet', collecting: 'Sammelt Daten', learning: 'Lernt', ready: 'Bereit', active: 'Aktiv', interrupted: 'Unterbrochen', expired: 'Unterbrochen', contradictory: 'Betriebsstatus nicht bestaetigt', 'no-plan': 'Sammelt Daten' })) {
|
||||
await showPlant(scenario);
|
||||
assert.equal(await page.locator('.forecast-workflow-state').innerText(), label, scenario);
|
||||
assert.equal(await page.locator('.forecast-planner details[open]').count(), 0, 'technical details remain collapsed');
|
||||
if (scenario === 'no-plan') assert.match(await page.locator('#host').innerText(), /Noch kein Netzfahrplan/);
|
||||
}
|
||||
await showPlant('hostile-reason');
|
||||
assert.equal(await page.locator('.forecast-workflow-reason img').count(), 0);
|
||||
assert.match(await page.locator('.forecast-workflow-reason').innerText(), /<img/);
|
||||
assert.equal(await page.evaluate(() => window.injected), undefined);
|
||||
await page.clock.install();
|
||||
await showPlant('active');
|
||||
assert.equal(await page.locator('.forecast-workflow-state').innerText(), 'Aktiv');
|
||||
await page.clock.fastForward(120000);
|
||||
assert.equal(await page.locator('.forecast-workflow-state').innerText(), 'Unterbrochen', 'open pages expire the local report without hiding charts');
|
||||
assert.equal(await page.locator('.forecast-planner .forecast-chart').count(), 6);
|
||||
await page.clock.resume();
|
||||
await page.evaluate(async () => {
|
||||
window.planner.dispose();
|
||||
window.planner = window.mountPlanner(document.querySelector('#host'), { plantId: 'legacy', session: {} });
|
||||
|
||||
Reference in New Issue
Block a user