evcc-io/tests/energy-history.spec.ts

210 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect, type Page, type Locator } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
test.use({ baseURL: baseUrl() });
const from = "2026-03-24T21:00:00+01:00";
const to = "2026-03-25T01:00:00+01:00";
test.beforeAll(async () => {
await start(undefined, "energy-history.sql");
});
test.afterAll(async () => {
await stop();
});
test.describe("api", () => {
test("15-minute resolution", async ({ request }) => {
const res = await request.get(`${baseUrl()}/api/history/energy`, {
params: { from, to },
});
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(data).toHaveLength(2);
const grid = data.find((s: { title: string }) => s.title === "grid");
const home = data.find((s: { title: string }) => s.title === "home");
expect(grid).toBeDefined();
expect(home).toBeDefined();
expect(grid.data).toHaveLength(6);
expect(home.data).toHaveLength(6);
expect(home.data[0].energy).toBeCloseTo(0.1, 4);
expect(home.data[0].returnEnergy).toBeCloseTo(0, 4);
expect(grid.data[0].energy).toBeCloseTo(0.5, 4);
expect(grid.data[0].returnEnergy).toBeCloseTo(0.1, 4);
});
test("day aggregation", async ({ request }) => {
const res = await request.get(`${baseUrl()}/api/history/energy`, {
params: { from, to, aggregate: "day" },
});
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(data).toHaveLength(2);
const grid = data.find((s: { title: string }) => s.title === "grid");
const home = data.find((s: { title: string }) => s.title === "home");
expect(grid).toBeDefined();
expect(home).toBeDefined();
expect(grid.data).toHaveLength(2);
expect(home.data).toHaveLength(2);
const [homeDay1, homeDay2] = home.data;
const [gridDay1, gridDay2] = grid.data;
// day 1 (2026-03-24): 4 slots
expect(homeDay1.energy).toBeCloseTo(0.4, 4);
expect(homeDay1.returnEnergy).toBeCloseTo(0, 4);
expect(gridDay1.energy).toBeCloseTo(2.0, 4);
expect(gridDay1.returnEnergy).toBeCloseTo(0.4, 4);
// day 2 (2026-03-25): 2 slots
expect(homeDay2.energy).toBeCloseTo(0.2, 4);
expect(homeDay2.returnEnergy).toBeCloseTo(0, 4);
expect(gridDay2.energy).toBeCloseTo(1.0, 4);
expect(gridDay2.returnEnergy).toBeCloseTo(0.2, 4);
});
});
async function gotoDay(page: Page, year: number, month: number, day: number): Promise<void> {
await page.goto(`/#/history?period=day&year=${year}&month=${month}&day=${day}`);
await expect(page.getByRole("heading", { name: /history/i }).first()).toBeVisible();
}
async function gotoMonth(page: Page, year: number, month: number): Promise<void> {
await page.goto(`/#/history?period=month&year=${year}&month=${month}`);
await expect(page.getByRole("heading", { name: /history/i }).first()).toBeVisible();
}
function chart(page: Page, group: string): Locator {
return page.getByTestId(`group-chart-${group}`);
}
function section(page: Page, group: string): Locator {
return page.getByTestId(`history-section-${group}`);
}
// Y-axis labels rendered by echarts: position=right uses text-anchor=start.
// DOM order: axis name first, then ticks bottom→top (min, ..., max).
async function yAxis(c: Locator): Promise<string[]> {
const els = c.locator('svg text[text-anchor="start"]');
await els.first().waitFor();
return els.allTextContents();
}
test.describe("axis and units", () => {
test("grid ±2 kW bidirectional, 1 decimal", async ({ page }) => {
await gotoDay(page, 2026, 3, 24);
expect(await yAxis(chart(page, "grid"))).toEqual(["kW", "-2.0", "-1.0", "0.0", "1.0", "2.0"]);
});
test("sub-1 kW switches to W, floored at 1000 W", async ({ page }) => {
await gotoDay(page, 2026, 4, 1);
expect(await yAxis(chart(page, "grid"))).toEqual(["W", "-1,000", "-500", "0", "500", "1,000"]);
});
test("import-only stays symmetric", async ({ page }) => {
await gotoDay(page, 2026, 4, 2);
expect(await yAxis(chart(page, "grid"))).toEqual(["kW", "-2.0", "-1.0", "0.0", "1.0", "2.0"]);
});
test("battery ±3 kW, 1 decimal", async ({ page }) => {
await gotoDay(page, 2026, 4, 3);
expect(await yAxis(chart(page, "battery"))).toEqual([
"kW",
"-3.0",
"-1.5",
"0.0",
"1.5",
"3.0",
]);
});
test("battery ±6 kW, integer labels", async ({ page }) => {
await gotoDay(page, 2026, 4, 4);
expect(await yAxis(chart(page, "battery"))).toEqual(["kW", "-6", "-3", "0", "3", "6"]);
});
test("stacked batteries: axis follows stacked sum, not per-entity max", async ({ page }) => {
await gotoDay(page, 2026, 4, 8);
expect(await yAxis(chart(page, "battery"))).toEqual([
"kW",
"-3.0",
"-1.5",
"0.0",
"1.5",
"3.0",
]);
});
test("unidirectional axis stays positive", async ({ page }) => {
await gotoDay(page, 2026, 4, 5);
// Unidirectional groups use echarts auto-scale (min: 0). Peak 1.6 kW → ticks 0..2.
expect(await yAxis(chart(page, "pv"))).toEqual(["kW", "0.0", "0.5", "1.0", "1.5", "2.0"]);
});
test("all-zero data: W axis at 1000 W", async ({ page }) => {
await gotoDay(page, 2026, 4, 6);
await expect(section(page, "pv")).toBeVisible();
expect(await yAxis(chart(page, "pv"))).toEqual(["W", "0", "250", "500", "750", "1,000"]);
});
test("stacked entities + overlay not clipped", async ({ page }) => {
await gotoDay(page, 2026, 5, 2);
// Stacked east+west peak = 1.8 kWh × 4 = 7.2 kW → niceCeil = 8.
expect(await yAxis(chart(page, "pv"))).toEqual(["kW", "0", "2", "4", "6", "8"]);
});
test("month view uses kWh unit", async ({ page }) => {
await gotoMonth(page, 2026, 6);
expect(await yAxis(chart(page, "battery"))).toEqual([
"kWh",
"-2.0",
"-1.0",
"0.0",
"1.0",
"2.0",
]);
});
});
test.describe("consumption breakdown", () => {
// 2026-04-07: home = 1.0 kWh, Kitchen = 0.4 kWh, Office = 0.3 kWh,
// virtual Others = home − meters = 0.3 kWh.
test("home total, meter legend, virtual Others", async ({ page }) => {
await gotoDay(page, 2026, 4, 7);
const consumption = section(page, "meter");
await expect(consumption).toBeVisible();
// Section total = home, not sum of meters.
await expect(consumption.getByRole("heading")).toContainText("1.0 kWh");
// Others (virtual) + explicit meters.
await expect(consumption.getByRole("button", { name: "Others 300 Wh" })).toBeVisible();
await expect(consumption.getByRole("button", { name: "Kitchen 400 Wh" })).toBeVisible();
await expect(consumption.getByRole("button", { name: "Office 300 Wh" })).toBeVisible();
});
test("entity focus rescales axis and resets on unfocus", async ({ page }) => {
await gotoDay(page, 2026, 4, 7);
const consumption = section(page, "meter");
const meterChart = chart(page, "meter");
// Stacked total per slot 1.0 kW → echarts auto-axis 0..1.2 in 1-decimal.
expect(await yAxis(meterChart)).toEqual(["kW", "0.0", "0.3", "0.6", "0.9", "1.2"]);
// Focus Kitchen → peak 400 W → unit switches to W, floored at 1000 W.
const kitchen = consumption.getByRole("button", { name: "Kitchen 400 Wh" });
await kitchen.click();
await expect.poll(() => yAxis(meterChart)).toEqual(["W", "0", "250", "500", "750", "1,000"]);
// Unfocus → axis returns to kW range, not stuck on 1000 W cap.
await kitchen.click();
await expect.poll(() => yAxis(meterChart)).toEqual(["kW", "0.0", "0.3", "0.6", "0.9", "1.2"]);
});
});