chore: Typescript: Rewrite tests (#21723)

Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
Maschga 2025-06-15 23:23:22 +02:00 • committed by GitHub
parent fda3b066db
commit d43ab9fa6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 2287 additions and 2154 deletions

View file

@ -26,7 +26,9 @@ function setProperty(obj: object, props: string[], value: any) {
const initialState: State = {
offline: false,
loadpoints: [], // ensure array type
// ensure array type
loadpoints: [],
vehicles: [],
};
const state = reactive(initialState);

View file

@ -31,6 +31,7 @@ export interface State {
currency?: CURRENCY;
fatal?: FatalError;
auth?: Auth;
vehicles: Vehicle[];
}
export interface LoadpointCompact {

33
package-lock.json generated
View file

@ -69,7 +69,9 @@
"@storybook/test": "^8.6.4",
"@storybook/vue3": "^8.6.4",
"@storybook/vue3-vite": "^8.6.4",
"@types/body-parser": "^1.19.6",
"@types/smoothscroll-polyfill": "^0.3.4",
"@types/wait-on": "^5.3.4",
"cross-env": "^7.0.3",
"eslint-plugin-storybook": "^0.12.0",
"prettier-plugin-sort-json": "^4.1.1",
@ -3391,6 +3393,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/bootstrap": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz",
@ -3406,6 +3419,16 @@
"integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==",
"license": "MIT"
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@ -3474,6 +3497,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/wait-on": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/@types/wait-on/-/wait-on-5.3.4.tgz",
"integrity": "sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",

View file

@ -88,6 +88,8 @@
"@storybook/test": "^8.6.4",
"@storybook/vue3": "^8.6.4",
"@storybook/vue3-vite": "^8.6.4",
"@types/body-parser": "^1.19.6",
"@types/wait-on": "^5.3.4",
"@types/smoothscroll-polyfill": "^0.3.4",
"cross-env": "^7.0.3",
"eslint-plugin-storybook": "^0.12.0",

View file

@ -86,7 +86,7 @@ test.describe("network requests", async () => {
test("no failed requests", async ({ page }) => {
await page.waitForLoadState("networkidle");
const failedRequests = [];
const failedRequests: string[] = [];
page.on("requestfailed", (request) => failedRequests.push(request.url()));
await page.reload();

View file

@ -38,7 +38,7 @@ test.describe("aux meter", async () => {
await expectModalHidden(meterModal);
// check
await expect(page.getByTestId("aux")).toBeVisible(1);
await expect(page.getByTestId("aux")).toBeVisible();
await expect(page.getByTestId("aux")).toContainText("Water heater");
await expect(page.getByTestId("aux")).toContainText("1.2 kW");
@ -47,7 +47,7 @@ test.describe("aux meter", async () => {
await page.reload();
// recheck
await expect(page.getByTestId("aux")).toBeVisible(1);
await expect(page.getByTestId("aux")).toBeVisible();
await expect(page.getByTestId("aux")).toContainText("Water heater");
await expect(page.getByTestId("aux")).toContainText("1.2 kW");

View file

@ -1,87 +0,0 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.beforeAll(async () => {
await start(CONFIG_GRID_ONLY);
});
test.afterAll(async () => {
await stop();
});
test.describe("battery meter", async () => {
test("create, edit and remove battery meter", async ({ page }) => {
await page.goto("/#/config");
await enableExperimental(page);
await expect(page.getByTestId("battery")).toHaveCount(0);
// create #1
await page.getByRole("button", { name: "Add solar or battery" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByRole("button", { name: "Add battery meter" }).click();
await meterModal.getByLabel("Title").fill("Demo Battery");
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
await meterModal.getByLabel("Power").fill("4000");
await meterModal.getByLabel("Charge").fill("80");
await expect(meterModal.getByLabel("Maximum AC power of the hybrid inverter")).toHaveCount(0);
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
await meterModal.getByRole("link", { name: "validate" }).click();
await expect(meterModal.getByTestId("device-tag-power")).toContainText("4.0 kW");
await expect(meterModal.getByTestId("device-tag-soc")).toContainText("80.0%");
await meterModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("battery")).toBeVisible(1);
await expect(page.getByTestId("battery")).toContainText("Demo Battery");
// edit #1
await page.getByTestId("battery").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await meterModal.getByLabel("Power").fill("5000");
await meterModal.getByRole("button", { name: "Validate & save" }).click();
await expectModalHidden(meterModal);
const battery = page.getByTestId("battery");
await expect(battery).toBeVisible(1);
await expect(battery).toContainText("Demo Battery");
await expect(battery.getByTestId("device-tag-power")).toContainText("5.0 kW");
await expect(battery.getByTestId("device-tag-soc")).toContainText("80.0%");
// restart and check in main ui
await restart(CONFIG_GRID_ONLY);
await page.goto("/");
await page.getByTestId("visualization").click();
await expect(page.getByTestId("energyflow")).toContainText("Battery discharging80%5.0 kW");
// delete #1
await page.goto("/#/config");
await page.getByTestId("battery").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await meterModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("battery")).toHaveCount(0);
});
test("advanced fields", async ({ page }) => {
await page.goto("/#/config");
await enableExperimental(page);
await page.getByRole("button", { name: "Add solar or battery" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByRole("button", { name: "Add battery meter" }).click();
await meterModal.getByLabel("Title").fill("Demo Battery");
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
await expect(meterModal.getByLabel("Meter reading")).not.toBeVisible();
await page.getByRole("button", { name: "Show advanced settings" }).click();
await expect(meterModal.getByLabel("Meter reading")).toBeVisible();
await page.getByRole("button", { name: "Hide advanced settings" }).click();
await expect(meterModal.getByLabel("Meter reading")).not.toBeVisible();
});
});

View file

@ -0,0 +1,89 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.beforeAll(async () => {
await start(CONFIG_GRID_ONLY);
});
test.afterAll(async () => {
await stop();
});
test.describe("battery meter", async () => {
test("create, edit and remove battery meter", async ({ page }) => {
await page.goto("/#/config");
await enableExperimental(page);
await expect(page.getByTestId("battery")).toHaveCount(0);
// create #1
await page.getByRole("button", { name: "Add solar or battery" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByRole("button", { name: "Add battery meter" }).click();
await meterModal.getByLabel("Title").fill("Demo Battery");
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
await meterModal.getByLabel("Power").fill("4000");
await meterModal.getByLabel("Charge").fill("80");
await expect(meterModal.getByLabel("Maximum AC power of the hybrid inverter")).toHaveCount(
0
);
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
await meterModal.getByRole("link", { name: "validate" }).click();
await expect(meterModal.getByTestId("device-tag-power")).toContainText("4.0 kW");
await expect(meterModal.getByTestId("device-tag-soc")).toContainText("80.0%");
await meterModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("battery")).toBeVisible();
await expect(page.getByTestId("battery")).toContainText("Demo Battery");
// edit #1
await page.getByTestId("battery").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await meterModal.getByLabel("Power").fill("5000");
await meterModal.getByRole("button", { name: "Validate & save" }).click();
await expectModalHidden(meterModal);
const battery = page.getByTestId("battery");
await expect(battery).toBeVisible();
await expect(battery).toContainText("Demo Battery");
await expect(battery.getByTestId("device-tag-power")).toContainText("5.0 kW");
await expect(battery.getByTestId("device-tag-soc")).toContainText("80.0%");
// restart and check in main ui
await restart(CONFIG_GRID_ONLY);
await page.goto("/");
await page.getByTestId("visualization").click();
await expect(page.getByTestId("energyflow")).toContainText("Battery discharging80%5.0 kW");
// delete #1
await page.goto("/#/config");
await page.getByTestId("battery").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await meterModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("battery")).toHaveCount(0);
});
test("advanced fields", async ({ page }) => {
await page.goto("/#/config");
await enableExperimental(page);
await page.getByRole("button", { name: "Add solar or battery" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByRole("button", { name: "Add battery meter" }).click();
await meterModal.getByLabel("Title").fill("Demo Battery");
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
await expect(meterModal.getByLabel("Meter reading")).not.toBeVisible();
await page.getByRole("button", { name: "Show advanced settings" }).click();
await expect(meterModal.getByLabel("Meter reading")).toBeVisible();
await page.getByRole("button", { name: "Hide advanced settings" }).click();
await expect(meterModal.getByLabel("Meter reading")).not.toBeVisible();
});
});

View file

@ -1,500 +0,0 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { expectModalVisible, expectModalHidden, editorClear, editorPaste } from "./utils";
const CONFIG_EMPTY = "config-empty.evcc.yaml";
const CONFIG_ONE_LP = "config-one-lp.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.afterEach(async () => {
await stop();
});
async function enableExperimental(page) {
await page
.getByTestId("generalconfig-experimental")
.getByRole("button", { name: "edit" })
.click();
await page.getByLabel("Experimental 🧪").click();
await page.getByRole("button", { name: "Close" }).click();
}
async function addDemoCharger(page) {
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Add charger" }).click();
const modal = page.getByTestId("charger-modal");
await expectModalVisible(modal);
await modal.getByLabel("Manufacturer").selectOption("Demo charger");
await modal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(modal);
await expectModalVisible(lpModal);
}
async function addDemoMeter(page, power = "0") {
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Add dedicated charger meter" }).click();
const modal = page.getByTestId("meter-modal");
await expectModalVisible(modal);
await modal.getByLabel("Manufacturer").selectOption("Demo meter");
await modal.getByLabel("Power").fill(power);
await modal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(modal);
await expectModalVisible(lpModal);
}
async function addVehicle(page, title) {
await page.getByRole("button", { name: "Add vehicle" }).click();
const modal = page.getByTestId("vehicle-modal");
await expectModalVisible(modal);
await modal.getByLabel("Manufacturer").selectOption("Generic vehicle (without API)");
await modal.getByLabel("Title").fill(title);
await modal.getByRole("button", { name: "Validate & save" }).click();
await expectModalHidden(modal);
}
async function newLoadpoint(page, title) {
const lpModal = page.getByTestId("loadpoint-modal");
await page.getByRole("button", { name: "Add charge point" }).click();
await expectModalVisible(lpModal);
await lpModal.getByLabel("Title").fill(title);
}
test.describe("loadpoint", async () => {
test("create, update and delete", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
const lpModal = page.getByTestId("loadpoint-modal");
const chargerModal = page.getByTestId("charger-modal");
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
// new loadpoint
await newLoadpoint(page, "Solar Carport");
await lpModal.getByRole("button", { name: "Add charger" }).click();
// add charger
await chargerModal.getByLabel("Manufacturer").selectOption("Demo charger");
await chargerModal.getByLabel("Charge status").selectOption("C");
await chargerModal.getByLabel("Power").fill("11000");
await chargerModal.getByRole("button", { name: "Save" }).click();
await expect(lpModal.getByLabel("Title")).toHaveValue("Solar Carport");
// create loadpoint
await lpModal.getByRole("button", { name: "Save" }).click();
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport");
await expect(page.getByTestId("loadpoint")).toContainText("charging");
await expect(page.getByTestId("loadpoint")).toContainText("11.0 kW");
// restart button appears
const restartButton = await page
.getByTestId("bottom-banner")
.getByRole("button", { name: "Restart" });
await expect(restartButton).toBeVisible();
// restart
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport");
// update loadpoint title
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByLabel("Title").fill("Solar Carport 2");
await lpModal.getByRole("button", { name: "Save" }).click();
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport 2");
// restart
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport 2");
// update loadpoint power
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByTestId("chargerPower-22kw").click();
// update charger mode
await expect(lpModal.getByRole("textbox", { name: "Charger" })).toHaveValue(
"Demo charger [db:1]"
);
await lpModal.getByRole("textbox", { name: "Charger" }).click();
await chargerModal.getByLabel("Charge status").selectOption("A");
await chargerModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(chargerModal);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// restart
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("not connected");
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByTestId("chargerPower-22kw")).toHaveClass(/active/);
await expect(lpModal.getByLabel("Title")).toHaveValue("Solar Carport 2");
await lpModal.getByRole("button", { name: "Close" }).click();
// delete loadpoint
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByRole("button", { name: "Delete" }).click();
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
// restart
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
});
test("priority", async ({ page }) => {
await start(CONFIG_ONE_LP);
await page.goto("/#/config");
await enableExperimental(page);
const lpModal = page.getByTestId("loadpoint-modal");
// loadpoint from yaml
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("Carport");
// add loadpoint via UI
await newLoadpoint(page, "Garage");
await addDemoCharger(page);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// two loadpoints
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
await expect(page.getByTestId("loadpoint").nth(0)).toContainText("Carport");
await expect(page.getByTestId("loadpoint").nth(1)).toContainText("Garage");
// second loadpoint: increase priority
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Priority")).toHaveValue("0 (default)");
await lpModal.getByLabel("Priority").selectOption("1");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// restart
await restart(CONFIG_ONE_LP);
await page.reload();
// check priorities
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Priority")).toHaveValue("1");
// change back to 0
await lpModal.getByLabel("Priority").selectOption("0 (default)");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// restart
await restart(CONFIG_ONE_LP);
await page.reload();
// check priorities
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Priority")).toHaveValue("0 (default)");
});
test("vehicle", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
const LP_1 = "Carport";
const LP_2 = "Garage";
const VEHICLE_1 = "Green Car";
const VEHICLE_2 = "Yellow Van";
const lpModal = page.getByTestId("loadpoint-modal");
// add loadpoint > no vehicle option
await newLoadpoint(page, LP_1);
await addDemoCharger(page);
await expect(lpModal.getByLabel("Default vehicle")).not.toBeVisible();
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// edit loadpoint
await page.getByTestId("loadpoint").nth(0).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal).toContainText("No vehicles are configured.");
await lpModal.getByRole("button", { name: "Close" }).click();
// add vehicles
for (const title of [VEHICLE_1, VEHICLE_2]) {
await addVehicle(page, title);
}
// set vehicle as default for loadpoint 1
await page.getByTestId("loadpoint").nth(0).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByTestId("loadpointPollMode-charging")).toHaveClass(/active/);
await expect(lpModal.getByRole("checkbox", { name: "Interpolate charge level" })).toBeChecked();
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_1);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// add second loadpoint
await newLoadpoint(page, LP_2);
await addDemoCharger(page);
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_2);
await lpModal.getByRole("button", { name: "Save" }).click();
// restart
await restart(CONFIG_EMPTY);
await page.reload();
// check loadpoint default vehicles
for (const [index, vehicle] of [VEHICLE_1, VEHICLE_2].entries()) {
await page.getByTestId("loadpoint").nth(index).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.locator("#loadpointParamVehicle option:checked")).toHaveText(vehicle);
await lpModal.getByRole("button", { name: "Close" }).click();
await expectModalHidden(lpModal);
}
});
test("keep mode", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add grid meter
await page.getByRole("button", { name: "Add grid meter" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
await meterModal.getByLabel("Power").fill("-1000");
await meterModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(meterModal);
// add a loadpoint with dummy charger,
const lpModal = page.getByTestId("loadpoint-modal");
await newLoadpoint(page, "Carport");
await addDemoCharger(page);
await lpModal.getByLabel("Default mode").selectOption("---");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await restart(CONFIG_EMPTY);
// change on main ui
await page.goto("/");
await expect(page.getByRole("button", { name: "Off" })).toHaveClass(/active/);
await page.getByRole("button", { name: "Solar", exact: true }).click();
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByRole("button", { name: "Solar", exact: true })).toHaveClass(/active/);
// change default mode in config to fast
await page.goto("/#/config");
// open first loadpoint
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByLabel("Default mode").selectOption("Fast");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await restart(CONFIG_EMPTY);
// check loadpoint mode
await page.goto("/");
await expect(page.getByRole("button", { name: "Fast" })).toHaveClass(/active/);
});
test("delete vehicle references", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add vehicle, add loadpoint, add charger
await addVehicle(page, "Porsche");
await addVehicle(page, "Tesla");
await newLoadpoint(page, "Garage");
await addDemoCharger(page);
const lpModal = page.getByTestId("loadpoint-modal");
await expectModalVisible(lpModal);
await lpModal.getByLabel("Default vehicle").selectOption("Porsche");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// delete vehicle
await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click();
const vehicleModal = page.getByTestId("vehicle-modal");
await expectModalVisible(vehicleModal);
await vehicleModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(vehicleModal);
// restart
await restart(CONFIG_EMPTY);
await page.reload();
// check loadpoint default vehicle
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Default vehicle")).toHaveValue("");
});
test("delete charger references", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint, add charger
await newLoadpoint(page, "Garage");
await addDemoCharger(page);
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// delete charger
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByRole("textbox", { name: "Charger" }).click();
const chargerModal = page.getByTestId("charger-modal");
await expectModalVisible(chargerModal);
await chargerModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(chargerModal);
// restart without saving loadpoint
await restart(CONFIG_EMPTY);
await page.reload();
// check loadpoint default vehicle
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
await expect(lpModal).toContainText("Configuring a charger is required.");
});
test("delete meter references", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint, add charger
await newLoadpoint(page, "Garage");
await addDemoCharger(page);
await addDemoMeter(page, "11000");
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await expect(page.getByTestId("loadpoint")).toContainText("11.0 kW");
// delete charger
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByRole("textbox", { name: "Meter" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(meterModal);
// restart without saving loadpoint
await restart(CONFIG_EMPTY);
await page.reload();
// check loadpoint default vehicle
await expect(page.getByTestId("loadpoint")).not.toContainText("11.0 kW");
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
await expect(
lpModal.getByRole("button", { name: "Add dedicated charger meter" })
).toBeVisible();
});
test("user-defined charger", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint
await newLoadpoint(page, "Carport");
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Add charger" }).click();
// add user-defined charger
const chargerModal = page.getByTestId("charger-modal");
await expectModalVisible(chargerModal);
await chargerModal.getByLabel("Manufacturer").selectOption("User-defined device");
await page.waitForLoadState("networkidle");
const editor = chargerModal.getByTestId("yaml-editor");
await expect(editor).toContainText("status: # charger status [A..F]");
await editorClear(editor, 10);
await editorPaste(
editor,
page,
`status:
source: const
value: 'C'
enabled:
source: const
value: true
enable:
source: js
script: console.log(enable)
maxcurrent:
source: js
script: console.log(maxcurrent)
power:
source: const
value: 11000`
);
const restResult = chargerModal.getByTestId("test-result");
await expect(restResult).toContainText("Status: unknown");
await restResult.getByRole("link", { name: "validate" }).click();
await expect(restResult).toContainText("Status: successful");
await expect(restResult).toContainText(["Status", "charging"].join(""));
await expect(restResult).toContainText(["Enabled", "yes"].join(""));
await expect(restResult).toContainText(["Power", "11.0 kW"].join(""));
// create
await chargerModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(chargerModal);
await expectModalVisible(lpModal);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("Carport");
// restart evcc
await restart(CONFIG_EMPTY);
await page.reload();
const lpEntry = page.getByTestId("loadpoint");
await expect(lpEntry).toHaveCount(1);
await expect(lpEntry).toContainText("Carport");
await expect(lpEntry).toContainText(["Status", "charging"].join(""));
await expect(lpEntry).toContainText(["Enabled", "yes"].join(""));
await expect(lpEntry).toContainText(["Power", "11.0 kW"].join(""));
await lpEntry.getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Charger").first()).toHaveValue("User-defined device [db:1]");
await lpModal.getByLabel("Charger").first().click();
await expectModalVisible(chargerModal);
await expect(chargerModal.getByLabel("Manufacturer")).toHaveValue("User-defined device");
await page.waitForLoadState("networkidle");
await expect(editor).toContainText("value: 'C'");
await expect(editor).toContainText("value: 11000");
});
});

View file

@ -0,0 +1,512 @@
import { test, expect, type Page } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { expectModalVisible, expectModalHidden, editorClear, editorPaste } from "./utils";
const CONFIG_EMPTY = "config-empty.evcc.yaml";
const CONFIG_ONE_LP = "config-one-lp.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.afterEach(async () => {
await stop();
});
async function enableExperimental(page: Page) {
await page
.getByTestId("generalconfig-experimental")
.getByRole("button", { name: "edit" })
.click();
await page.getByLabel("Experimental 🧪").click();
await page.getByRole("button", { name: "Close" }).click();
}
async function addDemoCharger(page: Page) {
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Add charger" }).click();
const modal = page.getByTestId("charger-modal");
await expectModalVisible(modal);
await modal.getByLabel("Manufacturer").selectOption("Demo charger");
await modal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(modal);
await expectModalVisible(lpModal);
}
async function addDemoMeter(page: Page, power = "0") {
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Add dedicated charger meter" }).click();
const modal = page.getByTestId("meter-modal");
await expectModalVisible(modal);
await modal.getByLabel("Manufacturer").selectOption("Demo meter");
await modal.getByLabel("Power").fill(power);
await modal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(modal);
await expectModalVisible(lpModal);
}
async function addVehicle(page: Page, title: string) {
await page.getByRole("button", { name: "Add vehicle" }).click();
const modal = page.getByTestId("vehicle-modal");
await expectModalVisible(modal);
await modal.getByLabel("Manufacturer").selectOption("Generic vehicle (without API)");
await modal.getByLabel("Title").fill(title);
await modal.getByRole("button", { name: "Validate & save" }).click();
await expectModalHidden(modal);
}
async function newLoadpoint(page: Page, title: string) {
const lpModal = page.getByTestId("loadpoint-modal");
await page.getByRole("button", { name: "Add charge point" }).click();
await expectModalVisible(lpModal);
await lpModal.getByLabel("Title").fill(title);
}
test.describe("loadpoint", async () => {
test("create, update and delete", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
const lpModal = page.getByTestId("loadpoint-modal");
const chargerModal = page.getByTestId("charger-modal");
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
// new loadpoint
await newLoadpoint(page, "Solar Carport");
await lpModal.getByRole("button", { name: "Add charger" }).click();
// add charger
await chargerModal.getByLabel("Manufacturer").selectOption("Demo charger");
await chargerModal.getByLabel("Charge status").selectOption("C");
await chargerModal.getByLabel("Power").fill("11000");
await chargerModal.getByRole("button", { name: "Save" }).click();
await expect(lpModal.getByLabel("Title")).toHaveValue("Solar Carport");
// create loadpoint
await lpModal.getByRole("button", { name: "Save" }).click();
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport");
await expect(page.getByTestId("loadpoint")).toContainText("charging");
await expect(page.getByTestId("loadpoint")).toContainText("11.0 kW");
// restart button appears
const restartButton = await page
.getByTestId("bottom-banner")
.getByRole("button", { name: "Restart" });
await expect(restartButton).toBeVisible();
// restart
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport");
// update loadpoint title
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByLabel("Title").fill("Solar Carport 2");
await lpModal.getByRole("button", { name: "Save" }).click();
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport 2");
// restart
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport 2");
// update loadpoint power
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByTestId("chargerPower-22kw").click();
// update charger mode
await expect(lpModal.getByRole("textbox", { name: "Charger" })).toHaveValue(
"Demo charger [db:1]"
);
await lpModal.getByRole("textbox", { name: "Charger" }).click();
await chargerModal.getByLabel("Charge status").selectOption("A");
await chargerModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(chargerModal);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// restart
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("not connected");
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByTestId("chargerPower-22kw")).toHaveClass(/active/);
await expect(lpModal.getByLabel("Title")).toHaveValue("Solar Carport 2");
await lpModal.getByRole("button", { name: "Close" }).click();
// delete loadpoint
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByRole("button", { name: "Delete" }).click();
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
// restart
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
});
test("priority", async ({ page }) => {
await start(CONFIG_ONE_LP);
await page.goto("/#/config");
await enableExperimental(page);
const lpModal = page.getByTestId("loadpoint-modal");
// loadpoint from yaml
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("Carport");
// add loadpoint via UI
await newLoadpoint(page, "Garage");
await addDemoCharger(page);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// two loadpoints
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
await expect(page.getByTestId("loadpoint").nth(0)).toContainText("Carport");
await expect(page.getByTestId("loadpoint").nth(1)).toContainText("Garage");
// second loadpoint: increase priority
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Priority")).toHaveValue("0 (default)");
await lpModal.getByLabel("Priority").selectOption("1");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// restart
await restart(CONFIG_ONE_LP);
await page.reload();
// check priorities
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Priority")).toHaveValue("1");
// change back to 0
await lpModal.getByLabel("Priority").selectOption("0 (default)");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// restart
await restart(CONFIG_ONE_LP);
await page.reload();
// check priorities
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Priority")).toHaveValue("0 (default)");
});
test("vehicle", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
const LP_1 = "Carport";
const LP_2 = "Garage";
const VEHICLE_1 = "Green Car";
const VEHICLE_2 = "Yellow Van";
const lpModal = page.getByTestId("loadpoint-modal");
// add loadpoint > no vehicle option
await newLoadpoint(page, LP_1);
await addDemoCharger(page);
await expect(lpModal.getByLabel("Default vehicle")).not.toBeVisible();
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// edit loadpoint
await page.getByTestId("loadpoint").nth(0).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal).toContainText("No vehicles are configured.");
await lpModal.getByRole("button", { name: "Close" }).click();
// add vehicles
for (const title of [VEHICLE_1, VEHICLE_2]) {
await addVehicle(page, title);
}
// set vehicle as default for loadpoint 1
await page.getByTestId("loadpoint").nth(0).getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByTestId("loadpointPollMode-charging")).toHaveClass(/active/);
await expect(
lpModal.getByRole("checkbox", { name: "Interpolate charge level" })
).toBeChecked();
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_1);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// add second loadpoint
await newLoadpoint(page, LP_2);
await addDemoCharger(page);
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_2);
await lpModal.getByRole("button", { name: "Save" }).click();
// restart
await restart(CONFIG_EMPTY);
await page.reload();
// check loadpoint default vehicles
for (const [index, vehicle] of [VEHICLE_1, VEHICLE_2].entries()) {
await page
.getByTestId("loadpoint")
.nth(index)
.getByRole("button", { name: "edit" })
.click();
await expectModalVisible(lpModal);
await expect(lpModal.locator("#loadpointParamVehicle option:checked")).toHaveText(
vehicle
);
await lpModal.getByRole("button", { name: "Close" }).click();
await expectModalHidden(lpModal);
}
});
test("keep mode", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add grid meter
await page.getByRole("button", { name: "Add grid meter" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
await meterModal.getByLabel("Power").fill("-1000");
await meterModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(meterModal);
// add a loadpoint with dummy charger,
const lpModal = page.getByTestId("loadpoint-modal");
await newLoadpoint(page, "Carport");
await addDemoCharger(page);
await lpModal.getByLabel("Default mode").selectOption("---");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await restart(CONFIG_EMPTY);
// change on main ui
await page.goto("/");
await expect(page.getByRole("button", { name: "Off" })).toHaveClass(/active/);
await page.getByRole("button", { name: "Solar", exact: true }).click();
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByRole("button", { name: "Solar", exact: true })).toHaveClass(
/active/
);
// change default mode in config to fast
await page.goto("/#/config");
// open first loadpoint
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByLabel("Default mode").selectOption("Fast");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await restart(CONFIG_EMPTY);
// check loadpoint mode
await page.goto("/");
await expect(page.getByRole("button", { name: "Fast" })).toHaveClass(/active/);
});
test("delete vehicle references", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add vehicle, add loadpoint, add charger
await addVehicle(page, "Porsche");
await addVehicle(page, "Tesla");
await newLoadpoint(page, "Garage");
await addDemoCharger(page);
const lpModal = page.getByTestId("loadpoint-modal");
await expectModalVisible(lpModal);
await lpModal.getByLabel("Default vehicle").selectOption("Porsche");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// delete vehicle
await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click();
const vehicleModal = page.getByTestId("vehicle-modal");
await expectModalVisible(vehicleModal);
await vehicleModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(vehicleModal);
// restart
await restart(CONFIG_EMPTY);
await page.reload();
// check loadpoint default vehicle
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Default vehicle")).toHaveValue("");
});
test("delete charger references", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint, add charger
await newLoadpoint(page, "Garage");
await addDemoCharger(page);
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
// delete charger
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByRole("textbox", { name: "Charger" }).click();
const chargerModal = page.getByTestId("charger-modal");
await expectModalVisible(chargerModal);
await chargerModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(chargerModal);
// restart without saving loadpoint
await restart(CONFIG_EMPTY);
await page.reload();
// check loadpoint default vehicle
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
await expect(lpModal).toContainText("Configuring a charger is required.");
});
test("delete meter references", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint, add charger
await newLoadpoint(page, "Garage");
await addDemoCharger(page);
await addDemoMeter(page, "11000");
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await expect(page.getByTestId("loadpoint")).toContainText("11.0 kW");
// delete charger
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByRole("textbox", { name: "Meter" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(meterModal);
// restart without saving loadpoint
await restart(CONFIG_EMPTY);
await page.reload();
// check loadpoint default vehicle
await expect(page.getByTestId("loadpoint")).not.toContainText("11.0 kW");
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
await expect(
lpModal.getByRole("button", { name: "Add dedicated charger meter" })
).toBeVisible();
});
test("user-defined charger", async ({ page }) => {
await start(CONFIG_EMPTY);
await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint
await newLoadpoint(page, "Carport");
const lpModal = page.getByTestId("loadpoint-modal");
await lpModal.getByRole("button", { name: "Add charger" }).click();
// add user-defined charger
const chargerModal = page.getByTestId("charger-modal");
await expectModalVisible(chargerModal);
await chargerModal.getByLabel("Manufacturer").selectOption("User-defined device");
await page.waitForLoadState("networkidle");
const editor = chargerModal.getByTestId("yaml-editor");
await expect(editor).toContainText("status: # charger status [A..F]");
await editorClear(editor, 10);
await editorPaste(
editor,
page,
`status:
source: const
value: 'C'
enabled:
source: const
value: true
enable:
source: js
script: console.log(enable)
maxcurrent:
source: js
script: console.log(maxcurrent)
power:
source: const
value: 11000`
);
const restResult = chargerModal.getByTestId("test-result");
await expect(restResult).toContainText("Status: unknown");
await restResult.getByRole("link", { name: "validate" }).click();
await expect(restResult).toContainText("Status: successful");
await expect(restResult).toContainText(["Status", "charging"].join(""));
await expect(restResult).toContainText(["Enabled", "yes"].join(""));
await expect(restResult).toContainText(["Power", "11.0 kW"].join(""));
// create
await chargerModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(chargerModal);
await expectModalVisible(lpModal);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("Carport");
// restart evcc
await restart(CONFIG_EMPTY);
await page.reload();
const lpEntry = page.getByTestId("loadpoint");
await expect(lpEntry).toHaveCount(1);
await expect(lpEntry).toContainText("Carport");
await expect(lpEntry).toContainText(["Status", "charging"].join(""));
await expect(lpEntry).toContainText(["Enabled", "yes"].join(""));
await expect(lpEntry).toContainText(["Power", "11.0 kW"].join(""));
await lpEntry.getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Charger").first()).toHaveValue(
"User-defined device [db:1]"
);
await lpModal.getByLabel("Charger").first().click();
await expectModalVisible(chargerModal);
await expect(chargerModal.getByLabel("Manufacturer")).toHaveValue("User-defined device");
await page.waitForLoadState("networkidle");
await expect(editor).toContainText("value: 'C'");
await expect(editor).toContainText("value: 11000");
});
});

View file

@ -1,44 +0,0 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.afterEach(async () => {
await stop();
});
const SELECT_ALL = "ControlOrMeta+KeyA";
async function goToConfig(page) {
await page.goto("/#/config");
await enableExperimental(page);
}
test.describe("messaging", async () => {
test("save a comment", async ({ page }) => {
await start(CONFIG_GRID_ONLY);
await goToConfig(page);
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
const modal = await page.getByTestId("messaging-modal");
await expectModalVisible(modal);
await modal.locator(".monaco-editor .view-line").nth(0).click();
for (let i = 0; i < 4; i++) {
await page.keyboard.press(SELECT_ALL, { delay: 10 });
await page.keyboard.press("Backspace", { delay: 10 });
}
await page.keyboard.type("# hello world");
await page.getByRole("button", { name: "Save" }).click();
await expectModalHidden(modal);
page.reload();
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
await expectModalVisible(modal);
await expect(modal).toContainText("# hello world");
});
});

View file

@ -0,0 +1,44 @@
import { test, expect, type Page } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.afterEach(async () => {
await stop();
});
const SELECT_ALL = "ControlOrMeta+KeyA";
async function goToConfig(page: Page) {
await page.goto("/#/config");
await enableExperimental(page);
}
test.describe("messaging", async () => {
test("save a comment", async ({ page }) => {
await start(CONFIG_GRID_ONLY);
await goToConfig(page);
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
const modal = await page.getByTestId("messaging-modal");
await expectModalVisible(modal);
await modal.locator(".monaco-editor .view-line").nth(0).click();
for (let i = 0; i < 4; i++) {
await page.keyboard.press(SELECT_ALL, { delay: 10 });
await page.keyboard.press("Backspace", { delay: 10 });
}
await page.keyboard.type("# hello world");
await page.getByRole("button", { name: "Save" }).click();
await expectModalHidden(modal);
page.reload();
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
await expectModalVisible(modal);
await expect(modal).toContainText("# hello world");
});
});

View file

@ -26,8 +26,12 @@ const VALID_USERNAME = "rw";
const VALID_PASSWORD = "readwrite";
test.describe("mqtt", async () => {
let mqttReachable = true;
test.beforeAll(async () => {
mqttReachable = await isMqttReachable(VALID_BROKER, VALID_USERNAME, VALID_PASSWORD);
});
test.skip(
async () => !(await isMqttReachable(VALID_BROKER, VALID_USERNAME, VALID_PASSWORD)),
!mqttReachable,
`MQTT broker ${VALID_BROKER} is not reachable, skipping tests`
);

View file

@ -1,114 +0,0 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorHost } from "./simulator";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.beforeAll(async () => {
await start(CONFIG_GRID_ONLY);
});
test.afterAll(async () => {
await stop();
});
test.describe("pv meter", async () => {
test("create, edit and remove pv meter", async ({ page }) => {
await page.goto("/#/config");
await enableExperimental(page);
await expect(page.getByTestId("pv")).toHaveCount(0);
// create #1
await page.getByRole("button", { name: "Add solar or battery" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
await meterModal.getByLabel("Title").fill("PV North");
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
await meterModal.getByLabel("Power optional").fill("5000");
await page.getByRole("button", { name: "Show advanced settings" }).click();
await expect(meterModal.getByLabel("Minimum charge")).not.toBeVisible(); // battery usage only
await expect(meterModal.getByLabel("Maximum AC power of the hybrid inverter")).toBeVisible(); // pv usage only
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
await meterModal.getByRole("link", { name: "validate" }).click();
await expect(meterModal.getByTestId("device-tag-power")).toContainText("5.0 kW");
await meterModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("pv")).toBeVisible(1);
await expect(page.getByTestId("pv")).toContainText("PV North");
// edit #1
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await meterModal.getByLabel("Power optional").fill("6000");
await meterModal.getByRole("button", { name: "Validate & save" }).click();
await expectModalHidden(meterModal);
const pv = page.getByTestId("pv");
await expect(pv).toBeVisible(1);
await expect(pv).toContainText("PV North");
await expect(pv.getByTestId("device-tag-power")).toContainText("6.0 kW");
// restart and check in main ui
await restart(CONFIG_GRID_ONLY);
await page.goto("/");
await page.getByTestId("visualization").click();
await expect(page.getByTestId("energyflow")).toContainText("Production6.0 kW");
// delete #1
await page.goto("/#/config");
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await meterModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("pv")).toHaveCount(0);
// restart and check again
await restart(CONFIG_GRID_ONLY);
await page.reload();
await expect(page.getByTestId("pv")).toHaveCount(0);
});
test("remove broken pv meter", async ({ page }) => {
// setup test data for mock openems api
await startSimulator();
await page.goto("/#/config");
await enableExperimental(page);
// create meter
await page.getByRole("button", { name: "Add solar or battery" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
await meterModal.getByLabel("Title").fill("North Roof");
await meterModal.getByLabel("Manufacturer").selectOption("shelly-1pm");
await meterModal.getByLabel("IP address or hostname").fill(simulatorHost());
await meterModal.getByRole("button", { name: "Validate & save" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("pv")).toBeVisible(1);
await expect(page.getByTestId("pv")).toContainText("North Roof");
// break meter
await stopSimulator();
await restart(CONFIG_GRID_ONLY);
await page.reload();
// remove meter
await expect(page.getByTestId("fatal-error")).toBeVisible();
await expect(page.getByTestId("pv")).toBeVisible(1);
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await meterModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("pv")).toHaveCount(0);
// restart and check again
await restart(CONFIG_GRID_ONLY);
await page.reload();
await expect(page.getByTestId("pv")).toHaveCount(0);
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
});
});

116
tests/config-pv.spec.ts Normal file
View file

@ -0,0 +1,116 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorHost } from "./simulator";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.beforeAll(async () => {
await start(CONFIG_GRID_ONLY);
});
test.afterAll(async () => {
await stop();
});
test.describe("pv meter", async () => {
test("create, edit and remove pv meter", async ({ page }) => {
await page.goto("/#/config");
await enableExperimental(page);
await expect(page.getByTestId("pv")).toHaveCount(0);
// create #1
await page.getByRole("button", { name: "Add solar or battery" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
await meterModal.getByLabel("Title").fill("PV North");
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
await meterModal.getByLabel("Power optional").fill("5000");
await page.getByRole("button", { name: "Show advanced settings" }).click();
await expect(meterModal.getByLabel("Minimum charge")).not.toBeVisible(); // battery usage only
await expect(
meterModal.getByLabel("Maximum AC power of the hybrid inverter")
).toBeVisible(); // pv usage only
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
await meterModal.getByRole("link", { name: "validate" }).click();
await expect(meterModal.getByTestId("device-tag-power")).toContainText("5.0 kW");
await meterModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("pv")).toBeVisible();
await expect(page.getByTestId("pv")).toContainText("PV North");
// edit #1
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await meterModal.getByLabel("Power optional").fill("6000");
await meterModal.getByRole("button", { name: "Validate & save" }).click();
await expectModalHidden(meterModal);
const pv = page.getByTestId("pv");
await expect(pv).toBeVisible();
await expect(pv).toContainText("PV North");
await expect(pv.getByTestId("device-tag-power")).toContainText("6.0 kW");
// restart and check in main ui
await restart(CONFIG_GRID_ONLY);
await page.goto("/");
await page.getByTestId("visualization").click();
await expect(page.getByTestId("energyflow")).toContainText("Production6.0 kW");
// delete #1
await page.goto("/#/config");
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await meterModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("pv")).toHaveCount(0);
// restart and check again
await restart(CONFIG_GRID_ONLY);
await page.reload();
await expect(page.getByTestId("pv")).toHaveCount(0);
});
test("remove broken pv meter", async ({ page }) => {
// setup test data for mock openems api
await startSimulator();
await page.goto("/#/config");
await enableExperimental(page);
// create meter
await page.getByRole("button", { name: "Add solar or battery" }).click();
const meterModal = page.getByTestId("meter-modal");
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
await meterModal.getByLabel("Title").fill("North Roof");
await meterModal.getByLabel("Manufacturer").selectOption("shelly-1pm");
await meterModal.getByLabel("IP address or hostname").fill(simulatorHost());
await meterModal.getByRole("button", { name: "Validate & save" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("pv")).toBeVisible();
await expect(page.getByTestId("pv")).toContainText("North Roof");
// break meter
await stopSimulator();
await restart(CONFIG_GRID_ONLY);
await page.reload();
// remove meter
await expect(page.getByTestId("fatal-error")).toBeVisible();
await expect(page.getByTestId("pv")).toBeVisible();
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
await expectModalVisible(meterModal);
await meterModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(meterModal);
await expect(page.getByTestId("pv")).toHaveCount(0);
// restart and check again
await restart(CONFIG_GRID_ONLY);
await page.reload();
await expect(page.getByTestId("pv")).toHaveCount(0);
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
});
});

View file

@ -1,106 +0,0 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import {
enableExperimental,
expectModalHidden,
expectModalVisible,
editorClear,
editorPaste,
} from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
const CONFIG_WITH_TARIFFS = "config-with-tariffs.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.describe.configure({ mode: "parallel" });
test.afterEach(async () => {
await stop();
});
async function goToConfig(page) {
await page.goto("/#/config");
await enableExperimental(page);
}
test.describe("tariffs", async () => {
test("tariffs not configured", async ({ page }) => {
await start(CONFIG_GRID_ONLY);
await goToConfig(page);
await expect(page.getByTestId("tariffs")).not.toBeVisible();
await expect(page.getByTestId("add-tariffs")).toBeVisible();
});
test("tariffs via ui", async ({ page }) => {
await start(CONFIG_GRID_ONLY);
await goToConfig(page);
await page.getByTestId("add-tariffs").click();
const modal = await page.getByTestId("tariffs-modal");
await expectModalVisible(modal);
await page.waitForLoadState("networkidle");
// default content
const editor = modal.getByTestId("yaml-editor");
await expect(editor).toContainText("#currency: EUR");
// clear and enter invalid yaml
await editorClear(editor);
await editorPaste(editor, page, "foo: bar");
await page.getByRole("button", { name: "Save" }).click();
await expect(modal.getByTestId("error")).toContainText("invalid keys: foo");
// clear and enter valid yaml
await editorClear(editor);
await editorPaste(
editor,
page,
`currency: CHF
grid:
type: fixed
price: 0.123`
);
await page.getByRole("button", { name: "Save" }).click();
await expect(modal.getByTestId("error")).not.toBeVisible();
// modal closes
await expectModalHidden(modal);
// restart button appears
const restartButton = await page
.getByTestId("bottom-banner")
.getByRole("button", { name: "Restart" });
await expect(restartButton).toBeVisible();
await restart(CONFIG_GRID_ONLY);
// restart done
await expect(restartButton).not.toBeVisible();
await expect(page.getByTestId("tariffs")).toContainText(
["Currency", "CHF", "Grid price", "12.3 rp"].join("")
);
});
test("tariffs from evcc.yaml", async ({ page }) => {
await start(CONFIG_WITH_TARIFFS);
await goToConfig(page);
await expect(page.getByTestId("tariffs")).toBeVisible();
await expect(page.getByTestId("tariffs")).toContainText(
[
"Tariffs",
"Currency",
"SEK",
"Grid price",
"30.0 öre",
"Feed-in price",
"-10.0 öre",
"Grid CO₂",
"300 g",
].join("")
);
});
});

View file

@ -0,0 +1,106 @@
import { test, expect, type Page } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import {
enableExperimental,
expectModalHidden,
expectModalVisible,
editorClear,
editorPaste,
} from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
const CONFIG_WITH_TARIFFS = "config-with-tariffs.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.describe.configure({ mode: "parallel" });
test.afterEach(async () => {
await stop();
});
async function goToConfig(page: Page) {
await page.goto("/#/config");
await enableExperimental(page);
}
test.describe("tariffs", async () => {
test("tariffs not configured", async ({ page }) => {
await start(CONFIG_GRID_ONLY);
await goToConfig(page);
await expect(page.getByTestId("tariffs")).not.toBeVisible();
await expect(page.getByTestId("add-tariffs")).toBeVisible();
});
test("tariffs via ui", async ({ page }) => {
await start(CONFIG_GRID_ONLY);
await goToConfig(page);
await page.getByTestId("add-tariffs").click();
const modal = await page.getByTestId("tariffs-modal");
await expectModalVisible(modal);
await page.waitForLoadState("networkidle");
// default content
const editor = modal.getByTestId("yaml-editor");
await expect(editor).toContainText("#currency: EUR");
// clear and enter invalid yaml
await editorClear(editor);
await editorPaste(editor, page, "foo: bar");
await page.getByRole("button", { name: "Save" }).click();
await expect(modal.getByTestId("error")).toContainText("invalid keys: foo");
// clear and enter valid yaml
await editorClear(editor);
await editorPaste(
editor,
page,
`currency: CHF
grid:
type: fixed
price: 0.123`
);
await page.getByRole("button", { name: "Save" }).click();
await expect(modal.getByTestId("error")).not.toBeVisible();
// modal closes
await expectModalHidden(modal);
// restart button appears
const restartButton = await page
.getByTestId("bottom-banner")
.getByRole("button", { name: "Restart" });
await expect(restartButton).toBeVisible();
await restart(CONFIG_GRID_ONLY);
// restart done
await expect(restartButton).not.toBeVisible();
await expect(page.getByTestId("tariffs")).toContainText(
["Currency", "CHF", "Grid price", "12.3 rp"].join("")
);
});
test("tariffs from evcc.yaml", async ({ page }) => {
await start(CONFIG_WITH_TARIFFS);
await goToConfig(page);
await expect(page.getByTestId("tariffs")).toBeVisible();
await expect(page.getByTestId("tariffs")).toContainText(
[
"Tariffs",
"Currency",
"SEK",
"Grid price",
"30.0 öre",
"Feed-in price",
"-10.0 öre",
"Grid CO₂",
"300 g",
].join("")
);
});
});

View file

@ -1,136 +0,0 @@
import fs from "fs";
import waitOn from "wait-on";
import axios from "axios";
import { spawn, execSync } from "child_process";
import os from "os";
import path from "path";
import { Transform } from "stream";
const BINARY = "./evcc";
function workerPort() {
const index = process.env.TEST_WORKER_INDEX * 1;
return 11000 + index;
}
function logPrefix() {
return `[worker:${process.env.TEST_WORKER_INDEX}]`;
}
function createSteamLog() {
return new Transform({
transform(chunk, encoding, callback) {
const lines = chunk.toString().split("\n");
lines.forEach((line) => {
if (line.trim()) log(line);
});
callback();
},
});
}
function log(...args) {
console.log(logPrefix(), ...args);
}
export function baseUrl() {
return `http://localhost:${workerPort()}`;
}
function dbPath() {
const file = `evcc-${workerPort()}.db`;
return path.join(os.tmpdir(), file);
}
export async function start(config, sqlDumps, flags = "--disable-auth") {
await _clean();
if (sqlDumps) {
await _restoreDatabase(sqlDumps);
}
return await _start(config, flags);
}
export async function stop(instance) {
await _stop(instance);
await _clean();
}
export async function restart(config, flags = "--disable-auth") {
await _stop();
await _start(config, flags);
}
export async function cleanRestart(config, sqlDumps) {
await _stop();
await _clean();
if (sqlDumps) {
await _restoreDatabase(sqlDumps);
}
await _start(config);
}
async function _restoreDatabase(sqlDumps) {
const dumps = Array.isArray(sqlDumps) ? sqlDumps : [sqlDumps];
for (const dump of dumps) {
log("loading database", dbPath(), dump);
execSync(`sqlite3 ${dbPath()} < tests/${dump}`);
}
}
async function _start(config, flags = []) {
const configFile = config.includes("/") ? config : `tests/${config}`;
const port = workerPort();
log(`wait until port ${port} is available`);
// wait for port to be available
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
const additionalFlags = typeof flags === "string" ? [flags] : flags;
additionalFlags.push("--log", "debug,httpd:trace");
log("starting evcc", { config, port, additionalFlags });
const instance = spawn(BINARY, ["--config", configFile, ...additionalFlags], {
env: { EVCC_NETWORK_PORT: port.toString(), EVCC_DATABASE_DSN: dbPath() },
stdio: ["pipe", "pipe", "pipe"],
});
const steamLog = createSteamLog();
instance.stdout.pipe(steamLog);
instance.stderr.pipe(steamLog);
instance.on("exit", (code) => {
log("evcc terminated", { code, port, config });
steamLog.end();
});
await waitOn({ resources: [baseUrl()], log: true });
return instance;
}
async function _stop(instance) {
const port = workerPort();
if (instance) {
log("shutting down evcc hard", { port });
instance.kill("SIGKILL");
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
log("evcc is down", { port });
return;
}
// check if auth is required
const res = await axios.get(`${baseUrl()}/api/auth/status`);
log("auth status", res.status, res.statusText, res.data);
let cookie;
// login required
if (!res.data) {
const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" });
log("login", res.status, res.statusText);
cookie = res.headers["set-cookie"];
}
log("shutting down evcc", { port });
await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } });
log(`wait until port ${port} is closed`);
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
log("evcc is down", { port });
}
async function _clean() {
const db = dbPath();
if (fs.existsSync(db)) {
log("delete database", db);
fs.unlinkSync(db);
}
}

140
tests/evcc.ts Normal file
View file

@ -0,0 +1,140 @@
import fs from "fs";
import waitOn from "wait-on";
import axios from "axios";
import { spawn, execSync, ChildProcess } from "child_process";
import os from "os";
import path from "path";
import { Transform } from "stream";
const BINARY = "./evcc";
function workerPort() {
const index = Number(process.env["TEST_WORKER_INDEX"] ?? 0);
return 11000 + index;
}
function logPrefix() {
return `[worker:${process.env["TEST_WORKER_INDEX"]}]`;
}
function createSteamLog() {
return new Transform({
transform(chunk: Buffer, _, callback) {
const lines = chunk.toString().split("\n");
lines.forEach((line: string) => {
if (line.trim()) log(line);
});
callback();
},
});
}
function log(...args: any[]) {
console.log(logPrefix(), ...args);
}
export function baseUrl() {
return `http://localhost:${workerPort()}`;
}
function dbPath() {
const file = `evcc-${workerPort()}.db`;
return path.join(os.tmpdir(), file);
}
export async function start(
config: string,
sqlDumps?: string | null,
flags: string | string[] = "--disable-auth"
) {
await _clean();
if (sqlDumps) {
await _restoreDatabase(sqlDumps);
}
return await _start(config, flags);
}
export async function stop(instance?: ChildProcess) {
await _stop(instance);
await _clean();
}
export async function restart(config: string, flags = "--disable-auth") {
await _stop();
await _start(config, flags);
}
export async function cleanRestart(config: string, sqlDumps: string) {
await _stop();
await _clean();
if (sqlDumps) {
await _restoreDatabase(sqlDumps);
}
await _start(config);
}
async function _restoreDatabase(sqlDumps: string) {
const dumps = Array.isArray(sqlDumps) ? sqlDumps : [sqlDumps];
for (const dump of dumps) {
log("loading database", dbPath(), dump);
execSync(`sqlite3 ${dbPath()} < tests/${dump}`);
}
}
async function _start(config: string, flags: string | string[] = []) {
const configFile = config.includes("/") ? config : `tests/${config}`;
const port = workerPort();
log(`wait until port ${port} is available`);
// wait for port to be available
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
const additionalFlags = typeof flags === "string" ? [flags] : flags;
additionalFlags.push("--log", "debug,httpd:trace");
log("starting evcc", { config, port, additionalFlags });
const instance = spawn(BINARY, ["--config", configFile, ...additionalFlags], {
env: { EVCC_NETWORK_PORT: port.toString(), EVCC_DATABASE_DSN: dbPath() },
stdio: ["pipe", "pipe", "pipe"],
});
const steamLog = createSteamLog();
instance.stdout.pipe(steamLog);
instance.stderr.pipe(steamLog);
instance.on("exit", (code) => {
log("evcc terminated", { code, port, config });
steamLog.end();
});
await waitOn({ resources: [baseUrl()], log: true });
return instance;
}
async function _stop(instance?: ChildProcess) {
const port = workerPort();
if (instance) {
log("shutting down evcc hard", { port });
instance.kill("SIGKILL");
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
log("evcc is down", { port });
return;
}
// check if auth is required
const res = await axios.get(`${baseUrl()}/api/auth/status`);
log("auth status", res.status, res.statusText, res.data);
let cookie;
// login required
if (!res.data) {
const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" });
log("login", res.status, res.statusText);
cookie = res.headers["set-cookie"];
}
log("shutting down evcc", { port });
await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } });
log(`wait until port ${port} is closed`);
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
log("evcc is down", { port });
}
async function _clean() {
const db = dbPath();
if (fs.existsSync(db)) {
log("delete database", db);
fs.unlinkSync(db);
}
}

View file

@ -1,630 +0,0 @@
import { test, expect, devices } from "@playwright/test";
import { start, stop, baseUrl, restart } from "./evcc";
test.use({ baseURL: baseUrl() });
test.describe.configure({ mode: "parallel" });
const mobile = devices["iPhone 12 Mini"].viewport;
const CONFIG = "plan.evcc.yaml";
const CONFIG_NO_TARIFF = "basics.evcc.yaml";
test.beforeEach(async () => {
await start(CONFIG);
});
test.afterEach(async () => {
await stop();
});
function getWeekday(offset = 1) {
const date = new Date();
date.setDate(date.getDate() + offset);
return date.toLocaleDateString("en-US", { weekday: "long" });
}
async function setAndVerifyPlan(page, lp, { soc, energy }) {
await lp.getByTestId("charging-plan-button").click();
if (soc) {
await page.getByTestId("static-plan-soc").selectOption(soc);
}
if (energy) {
// select "25 kWh (+50%)" by providing "25 kWh" as option text
const optionText = await page
.getByTestId("static-plan-energy")
.locator("option", { hasText: energy })
.textContent();
await page.getByTestId("static-plan-energy").selectOption(optionText);
}
await page.getByTestId("static-plan-active").click();
await page.getByRole("button", { name: "Close" }).click();
await expect(lp.getByTestId("charging-plan")).toContainText(soc || energy);
}
async function verifyRepeatingPlanAvailable(page, lp, expected) {
await lp.getByTestId("charging-plan-button").click();
if (expected) {
await expect(page.getByTestId("repeating-plan-add")).toBeVisible();
} else {
await expect(page.getByTestId("repeating-plan-add")).not.toBeVisible();
}
await page.getByRole("button", { name: "Close" }).click();
}
test.describe("basic functionality", async () => {
test("vehicle with soc and capacity, set and restart", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await expect(lp1.getByTestId("plan-marker")).not.toBeVisible();
await expect(lp1.getByText("Loadpoint", { exact: true })).toBeVisible();
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("90%");
await lp1.getByRole("button", { name: "Solar", exact: true }).click();
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByTestId("static-plan-day").selectOption({ index: 1 });
await page.getByTestId("static-plan-time").fill("09:30");
await page.getByTestId("static-plan-soc").selectOption("80%");
await page.getByTestId("static-plan-precondition-lg-toggle").click();
await page
.getByTestId("static-plan-precondition-lg-select")
.getByRole("combobox")
.selectOption("1 hour");
await page.getByTestId("static-plan-active").click();
await page.getByRole("button", { name: "Close" }).click();
await expect(lp1.getByTestId("plan-marker")).toBeVisible();
await expect(lp1.getByTestId("charging-plan").getByRole("button")).toHaveText(
"tomorrow 09:3080%"
);
await expect(lp1.getByTestId("vehicle-status-charger")).toHaveText("Connected.");
await expect(lp1.getByTestId("vehicle-status-planstart")).toHaveText(/tomorrow .*/);
await expect(lp1.getByTestId("plan-marker")).toBeVisible();
await expect(lp1.getByTestId("charging-plan").getByRole("button")).toHaveText(
"tomorrow 09:3080%"
);
await lp1.getByTestId("charging-plan").getByRole("button").click();
await expect(page.getByTestId("static-plan-soc")).toHaveValue("80");
});
});
test.describe("vehicle variations", async () => {
test.describe("guest vehicle", async () => {
test("kWh based plan and limit, no repeats", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
await expect(lp1.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
// kWh based limit
await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
// kWh based plan
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
// no repeating plans option
await verifyRepeatingPlanAvailable(page, lp1, false);
});
});
test.describe("vehicle no soc no capacity", async () => {
test("kWh based plan and limit, no repeats", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle no SoC no Capacity");
// kWh based limit
await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
// kWh based plan
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
// no repeating plans option
await verifyRepeatingPlanAvailable(page, lp1, false);
});
});
test.describe("vehicle no soc with capacity", async () => {
test("kWh based plan and limit, no repeats", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle no SoC with Capacity");
// kWh based limit
await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh (+50%)");
// kWh based plan
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
// no repeating plans option
await verifyRepeatingPlanAvailable(page, lp1, false);
});
});
test.describe("vehicle with soc no capacity", async () => {
test("kWh based plan and soc based limit, no repeats", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC no Capacity");
// soc based limit
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
// soc based plan
await setAndVerifyPlan(page, lp1, { energy: "50 kWh" });
// no repeating plans option
await verifyRepeatingPlanAvailable(page, lp1, false);
});
});
test.describe("vehicle with soc with capacity", async () => {
test("soc based plan and limit, with repeats", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
// soc based limit
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
// soc based plan
await setAndVerifyPlan(page, lp1, { soc: "60%" });
// repeating plans option
await verifyRepeatingPlanAvailable(page, lp1, true);
});
});
test.describe("loadpoint with soc, guest vehicle", async () => {
test("kWh based plan and soc based limit, no repeats", async ({ page }) => {
await page.goto("/");
const lp2 = await page.getByTestId("loadpoint").last();
// change vehicle
await expect(lp2.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
// soc based limit
await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
// soc based plan
await setAndVerifyPlan(page, lp2, { energy: "50 kWh" });
// no repeating plans option
await verifyRepeatingPlanAvailable(page, lp2, false);
});
});
test.describe("loadpoint with soc, vehicle with capacity", async () => {
test("soc based plan and limit, with repeats", async ({ page }) => {
await page.goto("/");
const lp2 = await page.getByTestId("loadpoint").last();
// change vehicle
await lp2
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle no SoC with Capacity");
// soc based limit
await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
// soc based plan
await setAndVerifyPlan(page, lp2, { soc: "60%" });
// repeating plans option
await verifyRepeatingPlanAvailable(page, lp2, true);
});
});
});
test.describe("preview", async () => {
const cases = [
{
szenario: "kWh based plan",
vehicle: "Vehicle no SoC with Capacity",
goalId: "static-plan-energy",
},
{
szenario: "soc based plan",
vehicle: "Vehicle with SoC with Capacity",
goalId: "static-plan-soc",
},
];
cases.forEach((c) => {
test(c.szenario, async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1.getByTestId("change-vehicle").locator("select").selectOption(c.vehicle);
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
// initial set -> preview plan
await page.getByTestId("static-plan-day").selectOption({ index: 1 });
await page.getByTestId("static-plan-time").fill("09:30");
await page.getByTestId(c.goalId).selectOption("80");
await expect(page.getByTestId("plan-preview-title")).toHaveText("Preview plan");
// activate -> active plan
await page.getByTestId("static-plan-active").click();
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
await page.getByTestId(c.goalId).selectOption("90");
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
// apply -> active plan
await expect(page.getByTestId("static-plan-apply")).toBeVisible();
await page.getByTestId("static-plan-apply").click();
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
await page.getByTestId("static-plan-time").fill("23:30");
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
await expect(page.getByTestId("static-plan-apply")).toBeVisible();
// deactivate
await page.getByTestId("static-plan-active").click();
await expect(page.getByTestId("plan-preview-title")).toHaveText("Preview plan");
});
});
});
test.describe("warnings", async () => {
test("goal not reachable in time", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Massive Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByTestId("static-plan-active").click();
// match this text but with fuzzy date "getByText('Goal will be reached 52:10 h')"
await expect(page.getByTestId("plan-warnings")).toHaveText(/Goal will be reached .* later/);
});
test("time in the past", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await expect(page.getByTestId("plan-entry-warnings")).not.toBeVisible();
await page.getByTestId("static-plan-day").selectOption({ index: 0 });
await page.getByTestId("static-plan-time").fill("00:01");
await expect(page.getByTestId("plan-entry-warnings")).toContainText(
"Pick a time in the future, Marty."
);
await page.getByTestId("static-plan-time").fill("00:01");
});
});
test.describe("repeating", async () => {
test("add and remove plans", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = await page.getByTestId("charging-plan-modal");
// one static plan, no number
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
await expect(modal.getByTestId("plan-entry").first()).not.toContainText("#1");
// add plan
await modal.getByRole("button", { name: "Add repeating plan" }).click();
await expect(modal.getByTestId("plan-entry")).toHaveCount(2);
await expect(modal.getByTestId("plan-entry").first()).toContainText("#1");
await expect(modal.getByTestId("plan-entry").last()).toContainText("#2");
// remove plan
await modal.getByTestId("plan-entry").last().getByRole("button", { name: "Remove" }).click();
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
await expect(modal.getByTestId("plan-entry").first()).not.toContainText("#1");
});
test("preview static and repeating plan", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = await page.getByTestId("charging-plan-modal");
// one static plan
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
await modal.getByTestId("static-plan-day").selectOption({ index: 1 }); // tomorrow
await modal.getByTestId("static-plan-time").fill("09:00");
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Preview plan");
await expect(modal.getByTestId("target-text")).toContainText("09:00");
// add repeating plan
await modal.getByRole("button", { name: "Add repeating plan" }).click();
await modal.getByTestId("repeating-plan-weekdays").click();
await modal.getByRole("checkbox", { name: "Select all" }).check();
await modal.getByTestId("repeating-plan-time").fill("11:11");
// switch between previews
await modal
.getByTestId("plan-preview-title")
.getByRole("combobox")
.selectOption("Preview plan #2");
await expect(modal.getByTestId("target-text")).toContainText("11:11");
await modal
.getByTestId("plan-preview-title")
.getByRole("combobox")
.selectOption("Preview plan #1");
await expect(modal.getByTestId("target-text")).toContainText("09:00");
// activate #1
await modal.getByTestId("static-plan-active").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #1");
await expect(modal.getByTestId("target-text")).toContainText("09:00");
// activate #2
await modal.getByTestId("static-plan-active").click();
await modal.getByTestId("repeating-plan-active").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("11:11");
// back to preview if no active plan
await modal.getByTestId("repeating-plan-active").click();
await expect(modal.getByTestId("plan-preview-title").locator("option:checked")).toHaveText(
"Preview plan #1"
);
await expect(modal.getByTestId("target-text")).toContainText("9:00");
});
test("weekday selection", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = await page.getByTestId("charging-plan-modal");
await modal.getByRole("button", { name: "Add repeating plan" }).click();
// weekday select should have value "Mo-Fr"
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
"Mon – Fri"
);
// select all weekdays
await modal.getByTestId("repeating-plan-weekdays").click();
await modal.getByRole("checkbox", { name: "Select all" }).click();
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
"Mon – Sun"
);
// select none
await modal.getByRole("checkbox", { name: "Select all" }).click();
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText("–");
// select specific weekdays
await modal.getByRole("checkbox", { name: "Thursday" }).check();
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
"Thu"
);
await modal.getByTestId("repeating-plan-weekdays").click(); // close
// activate
await modal.getByTestId("repeating-plan-time").fill("02:22");
await modal.getByTestId("repeating-plan-active").click();
// specific weekday and time
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("Thu 02:22");
});
test("next plan", async ({ page }) => {
await page.goto("/");
const yesterday = getWeekday(-1);
const tomorrow = getWeekday(1);
const lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = await page.getByTestId("charging-plan-modal");
// configure static plan for tomorrow
const plan1 = modal.getByTestId("plan-entry").nth(0);
await plan1.getByTestId("static-plan-day").selectOption({ index: 1 });
await plan1.getByTestId("static-plan-time").fill("09:30");
await plan1.getByTestId("static-plan-soc").selectOption("80%");
await plan1.getByTestId("static-plan-active").click();
// add repeating plan for tomorrow
await modal.getByRole("button", { name: "Add repeating plan" }).click();
const plan2 = modal.getByTestId("plan-entry").nth(1);
const days2 = plan2.getByTestId("repeating-plan-weekdays");
await days2.click();
await days2.getByRole("checkbox", { name: "Select all" }).click();
await days2.getByRole("checkbox", { name: "Select all" }).click();
await days2.getByRole("checkbox", { name: tomorrow }).check();
await days2.click(); // close
await plan2.getByTestId("repeating-plan-time").fill("09:20");
await plan2.getByTestId("repeating-plan-active").click();
// add repeating plan for every day
await modal.getByRole("button", { name: "Add repeating plan" }).click();
const plan3 = modal.getByTestId("plan-entry").last();
const days3 = plan3.getByTestId("repeating-plan-weekdays");
await days3.click();
await days3.getByRole("checkbox", { name: "Select all" }).check();
await days3.click(); // close
await plan3.getByTestId("repeating-plan-time").fill("09:10");
await plan3.getByTestId("repeating-plan-active").click();
// check next plans
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #3");
await expect(modal.getByTestId("target-text")).toContainText("9:10");
// disable plan #3
await plan3.getByTestId("repeating-plan-active").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("9:20");
// change plan #2 to yesterday
await days2.click();
await days2.getByRole("checkbox", { name: tomorrow }).click(); // uncheck
await days2.getByRole("checkbox", { name: yesterday }).click(); // check
await days2.click(); // close
// no changes yet
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("9:20");
// apply
await plan2.getByTestId("repeating-plan-apply").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #1");
await expect(modal.getByTestId("target-text")).toContainText("9:30");
// set lower targets than vehicle soc (50%)
await plan1.getByTestId("static-plan-soc").selectOption("40%");
await plan1.getByTestId("static-plan-apply").click();
await plan2.getByTestId("repeating-plan-soc").selectOption("40%");
await plan2.getByTestId("repeating-plan-apply").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Goal already reached");
});
test("repeating plan persistence", async ({ page }) => {
await page.goto("/");
const tomorrow = getWeekday(1);
let lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
let modal = await page.getByTestId("charging-plan-modal");
await modal.getByRole("button", { name: "Add repeating plan" }).click();
const plan = modal.getByTestId("plan-entry").nth(1);
await plan.getByTestId("repeating-plan-weekdays").click();
await plan.getByRole("checkbox", { name: "Select all" }).click(); // check all
await plan.getByRole("checkbox", { name: "Select all" }).click(); // uncheck all
await plan.getByRole("checkbox", { name: tomorrow }).check();
await plan.getByTestId("repeating-plan-time").fill("09:20");
await plan.getByTestId("repeating-plan-precondition-lg-toggle").click();
await plan
.getByTestId("repeating-plan-precondition-lg-select")
.getByRole("combobox")
.selectOption("2 hours");
await plan.getByTestId("repeating-plan-active").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("09:20");
await restart(CONFIG);
await page.goto("/");
lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "tomorrow 09:20" }).click();
modal = await page.getByTestId("charging-plan-modal");
await expect(modal.getByTestId("plan-entry")).toHaveCount(2);
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("09:20");
await expect(modal.getByTestId("repeating-plan-precondition-lg-toggle")).toBeChecked();
await expect(
modal.getByTestId("repeating-plan-precondition-lg-select").locator("option:checked")
).toHaveText("2 hours");
});
});
// add test for precondition, start with basic.evcc.yaml and verify that precondition toggle element is not visible. make dedicated describe block
test.describe("precondition", async () => {
test("only if dynamic tariff exists", async ({ page }) => {
await restart(CONFIG_NO_TARIFF);
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await expect(page.getByTestId("static-plan-active")).toBeVisible();
await expect(page.getByTestId("static-plan-precondition-lg-toggle")).not.toBeVisible();
await expect(page.getByTestId("static-plan-precondition-lg-select")).not.toBeVisible();
// verify small viewport
await page.setViewportSize(mobile);
await expect(page.getByTestId("static-plan-precondition-select")).not.toBeVisible();
});
});

646
tests/plan.spec.ts Normal file
View file

@ -0,0 +1,646 @@
import { test, expect, devices, type Page, type Locator } from "@playwright/test";
import { start, stop, baseUrl, restart } from "./evcc";
test.use({ baseURL: baseUrl() });
test.describe.configure({ mode: "parallel" });
const mobile = devices["iPhone 12 Mini"].viewport;
const CONFIG = "plan.evcc.yaml";
const CONFIG_NO_TARIFF = "basics.evcc.yaml";
test.beforeEach(async () => {
await start(CONFIG);
});
test.afterEach(async () => {
await stop();
});
function getWeekday(offset = 1) {
const date = new Date();
date.setDate(date.getDate() + offset);
return date.toLocaleDateString("en-US", { weekday: "long" });
}
async function setAndVerifyPlan(
page: Page,
lp: Locator,
{ soc, energy }: { soc?: string; energy?: string }
) {
await lp.getByTestId("charging-plan-button").click();
if (soc) {
await page.getByTestId("static-plan-soc").selectOption(soc);
}
if (energy) {
// select "25 kWh (+50%)" by providing "25 kWh" as option text
const optionText = await page
.getByTestId("static-plan-energy")
.locator("option", { hasText: energy })
.textContent();
await page.getByTestId("static-plan-energy").selectOption(optionText);
}
await page.getByTestId("static-plan-active").click();
await page.getByRole("button", { name: "Close" }).click();
await expect(lp.getByTestId("charging-plan")).toContainText(soc || energy!);
}
async function verifyRepeatingPlanAvailable(page: Page, lp: Locator, expected: boolean) {
await lp.getByTestId("charging-plan-button").click();
if (expected) {
await expect(page.getByTestId("repeating-plan-add")).toBeVisible();
} else {
await expect(page.getByTestId("repeating-plan-add")).not.toBeVisible();
}
await page.getByRole("button", { name: "Close" }).click();
}
test.describe("basic functionality", async () => {
test("vehicle with soc and capacity, set and restart", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await expect(lp1.getByTestId("plan-marker")).not.toBeVisible();
await expect(lp1.getByText("Loadpoint", { exact: true })).toBeVisible();
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("90%");
await lp1.getByRole("button", { name: "Solar", exact: true }).click();
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByTestId("static-plan-day").selectOption({ index: 1 });
await page.getByTestId("static-plan-time").fill("09:30");
await page.getByTestId("static-plan-soc").selectOption("80%");
await page.getByTestId("static-plan-precondition-lg-toggle").click();
await page
.getByTestId("static-plan-precondition-lg-select")
.getByRole("combobox")
.selectOption("1 hour");
await page.getByTestId("static-plan-active").click();
await page.getByRole("button", { name: "Close" }).click();
await expect(lp1.getByTestId("plan-marker")).toBeVisible();
await expect(lp1.getByTestId("charging-plan").getByRole("button")).toHaveText(
"tomorrow 09:3080%"
);
await expect(lp1.getByTestId("vehicle-status-charger")).toHaveText("Connected.");
await expect(lp1.getByTestId("vehicle-status-planstart")).toHaveText(/tomorrow .*/);
await expect(lp1.getByTestId("plan-marker")).toBeVisible();
await expect(lp1.getByTestId("charging-plan").getByRole("button")).toHaveText(
"tomorrow 09:3080%"
);
await lp1.getByTestId("charging-plan").getByRole("button").click();
await expect(page.getByTestId("static-plan-soc")).toHaveValue("80");
});
});
test.describe("vehicle variations", async () => {
test.describe("guest vehicle", async () => {
test("kWh based plan and limit, no repeats", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
await expect(lp1.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
// kWh based limit
await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
// kWh based plan
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
// no repeating plans option
await verifyRepeatingPlanAvailable(page, lp1, false);
});
});
test.describe("vehicle no soc no capacity", async () => {
test("kWh based plan and limit, no repeats", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle no SoC no Capacity");
// kWh based limit
await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
// kWh based plan
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
// no repeating plans option
await verifyRepeatingPlanAvailable(page, lp1, false);
});
});
test.describe("vehicle no soc with capacity", async () => {
test("kWh based plan and limit, no repeats", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle no SoC with Capacity");
// kWh based limit
await lp1
.getByTestId("limit-energy")
.getByRole("combobox")
.selectOption("50 kWh (+50%)");
// kWh based plan
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
// no repeating plans option
await verifyRepeatingPlanAvailable(page, lp1, false);
});
});
test.describe("vehicle with soc no capacity", async () => {
test("kWh based plan and soc based limit, no repeats", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC no Capacity");
// soc based limit
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
// soc based plan
await setAndVerifyPlan(page, lp1, { energy: "50 kWh" });
// no repeating plans option
await verifyRepeatingPlanAvailable(page, lp1, false);
});
});
test.describe("vehicle with soc with capacity", async () => {
test("soc based plan and limit, with repeats", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
// soc based limit
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
// soc based plan
await setAndVerifyPlan(page, lp1, { soc: "60%" });
// repeating plans option
await verifyRepeatingPlanAvailable(page, lp1, true);
});
});
test.describe("loadpoint with soc, guest vehicle", async () => {
test("kWh based plan and soc based limit, no repeats", async ({ page }) => {
await page.goto("/");
const lp2 = await page.getByTestId("loadpoint").last();
// change vehicle
await expect(lp2.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
// soc based limit
await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
// soc based plan
await setAndVerifyPlan(page, lp2, { energy: "50 kWh" });
// no repeating plans option
await verifyRepeatingPlanAvailable(page, lp2, false);
});
});
test.describe("loadpoint with soc, vehicle with capacity", async () => {
test("soc based plan and limit, with repeats", async ({ page }) => {
await page.goto("/");
const lp2 = await page.getByTestId("loadpoint").last();
// change vehicle
await lp2
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle no SoC with Capacity");
// soc based limit
await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
// soc based plan
await setAndVerifyPlan(page, lp2, { soc: "60%" });
// repeating plans option
await verifyRepeatingPlanAvailable(page, lp2, true);
});
});
});
test.describe("preview", async () => {
const cases = [
{
szenario: "kWh based plan",
vehicle: "Vehicle no SoC with Capacity",
goalId: "static-plan-energy",
},
{
szenario: "soc based plan",
vehicle: "Vehicle with SoC with Capacity",
goalId: "static-plan-soc",
},
];
cases.forEach((c) => {
test(c.szenario, async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1.getByTestId("change-vehicle").locator("select").selectOption(c.vehicle);
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
// initial set -> preview plan
await page.getByTestId("static-plan-day").selectOption({ index: 1 });
await page.getByTestId("static-plan-time").fill("09:30");
await page.getByTestId(c.goalId).selectOption("80");
await expect(page.getByTestId("plan-preview-title")).toHaveText("Preview plan");
// activate -> active plan
await page.getByTestId("static-plan-active").click();
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
await page.getByTestId(c.goalId).selectOption("90");
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
// apply -> active plan
await expect(page.getByTestId("static-plan-apply")).toBeVisible();
await page.getByTestId("static-plan-apply").click();
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
await page.getByTestId("static-plan-time").fill("23:30");
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
await expect(page.getByTestId("static-plan-apply")).toBeVisible();
// deactivate
await page.getByTestId("static-plan-active").click();
await expect(page.getByTestId("plan-preview-title")).toHaveText("Preview plan");
});
});
});
test.describe("warnings", async () => {
test("goal not reachable in time", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Massive Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByTestId("static-plan-active").click();
// match this text but with fuzzy date "getByText('Goal will be reached 52:10 h')"
await expect(page.getByTestId("plan-warnings")).toHaveText(/Goal will be reached .* later/);
});
test("time in the past", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
// change vehicle
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await expect(page.getByTestId("plan-entry-warnings")).not.toBeVisible();
await page.getByTestId("static-plan-day").selectOption({ index: 0 });
await page.getByTestId("static-plan-time").fill("00:01");
await expect(page.getByTestId("plan-entry-warnings")).toContainText(
"Pick a time in the future, Marty."
);
await page.getByTestId("static-plan-time").fill("00:01");
});
});
test.describe("repeating", async () => {
test("add and remove plans", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = await page.getByTestId("charging-plan-modal");
// one static plan, no number
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
await expect(modal.getByTestId("plan-entry").first()).not.toContainText("#1");
// add plan
await modal.getByRole("button", { name: "Add repeating plan" }).click();
await expect(modal.getByTestId("plan-entry")).toHaveCount(2);
await expect(modal.getByTestId("plan-entry").first()).toContainText("#1");
await expect(modal.getByTestId("plan-entry").last()).toContainText("#2");
// remove plan
await modal
.getByTestId("plan-entry")
.last()
.getByRole("button", { name: "Remove" })
.click();
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
await expect(modal.getByTestId("plan-entry").first()).not.toContainText("#1");
});
test("preview static and repeating plan", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = await page.getByTestId("charging-plan-modal");
// one static plan
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
await modal.getByTestId("static-plan-day").selectOption({ index: 1 }); // tomorrow
await modal.getByTestId("static-plan-time").fill("09:00");
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Preview plan");
await expect(modal.getByTestId("target-text")).toContainText("09:00");
// add repeating plan
await modal.getByRole("button", { name: "Add repeating plan" }).click();
await modal.getByTestId("repeating-plan-weekdays").click();
await modal.getByRole("checkbox", { name: "Select all" }).check();
await modal.getByTestId("repeating-plan-time").fill("11:11");
// switch between previews
await modal
.getByTestId("plan-preview-title")
.getByRole("combobox")
.selectOption("Preview plan #2");
await expect(modal.getByTestId("target-text")).toContainText("11:11");
await modal
.getByTestId("plan-preview-title")
.getByRole("combobox")
.selectOption("Preview plan #1");
await expect(modal.getByTestId("target-text")).toContainText("09:00");
// activate #1
await modal.getByTestId("static-plan-active").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #1");
await expect(modal.getByTestId("target-text")).toContainText("09:00");
// activate #2
await modal.getByTestId("static-plan-active").click();
await modal.getByTestId("repeating-plan-active").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("11:11");
// back to preview if no active plan
await modal.getByTestId("repeating-plan-active").click();
await expect(modal.getByTestId("plan-preview-title").locator("option:checked")).toHaveText(
"Preview plan #1"
);
await expect(modal.getByTestId("target-text")).toContainText("9:00");
});
test("weekday selection", async ({ page }) => {
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = await page.getByTestId("charging-plan-modal");
await modal.getByRole("button", { name: "Add repeating plan" }).click();
// weekday select should have value "Mo-Fr"
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
"Mon – Fri"
);
// select all weekdays
await modal.getByTestId("repeating-plan-weekdays").click();
await modal.getByRole("checkbox", { name: "Select all" }).click();
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
"Mon – Sun"
);
// select none
await modal.getByRole("checkbox", { name: "Select all" }).click();
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
"–"
);
// select specific weekdays
await modal.getByRole("checkbox", { name: "Thursday" }).check();
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
"Thu"
);
await modal.getByTestId("repeating-plan-weekdays").click(); // close
// activate
await modal.getByTestId("repeating-plan-time").fill("02:22");
await modal.getByTestId("repeating-plan-active").click();
// specific weekday and time
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("Thu 02:22");
});
test("next plan", async ({ page }) => {
await page.goto("/");
const yesterday = getWeekday(-1);
const tomorrow = getWeekday(1);
const lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = await page.getByTestId("charging-plan-modal");
// configure static plan for tomorrow
const plan1 = modal.getByTestId("plan-entry").nth(0);
await plan1.getByTestId("static-plan-day").selectOption({ index: 1 });
await plan1.getByTestId("static-plan-time").fill("09:30");
await plan1.getByTestId("static-plan-soc").selectOption("80%");
await plan1.getByTestId("static-plan-active").click();
// add repeating plan for tomorrow
await modal.getByRole("button", { name: "Add repeating plan" }).click();
const plan2 = modal.getByTestId("plan-entry").nth(1);
const days2 = plan2.getByTestId("repeating-plan-weekdays");
await days2.click();
await days2.getByRole("checkbox", { name: "Select all" }).click();
await days2.getByRole("checkbox", { name: "Select all" }).click();
await days2.getByRole("checkbox", { name: tomorrow }).check();
await days2.click(); // close
await plan2.getByTestId("repeating-plan-time").fill("09:20");
await plan2.getByTestId("repeating-plan-active").click();
// add repeating plan for every day
await modal.getByRole("button", { name: "Add repeating plan" }).click();
const plan3 = modal.getByTestId("plan-entry").last();
const days3 = plan3.getByTestId("repeating-plan-weekdays");
await days3.click();
await days3.getByRole("checkbox", { name: "Select all" }).check();
await days3.click(); // close
await plan3.getByTestId("repeating-plan-time").fill("09:10");
await plan3.getByTestId("repeating-plan-active").click();
// check next plans
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #3");
await expect(modal.getByTestId("target-text")).toContainText("9:10");
// disable plan #3
await plan3.getByTestId("repeating-plan-active").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("9:20");
// change plan #2 to yesterday
await days2.click();
await days2.getByRole("checkbox", { name: tomorrow }).click(); // uncheck
await days2.getByRole("checkbox", { name: yesterday }).click(); // check
await days2.click(); // close
// no changes yet
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("9:20");
// apply
await plan2.getByTestId("repeating-plan-apply").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #1");
await expect(modal.getByTestId("target-text")).toContainText("9:30");
// set lower targets than vehicle soc (50%)
await plan1.getByTestId("static-plan-soc").selectOption("40%");
await plan1.getByTestId("static-plan-apply").click();
await plan2.getByTestId("repeating-plan-soc").selectOption("40%");
await plan2.getByTestId("repeating-plan-apply").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Goal already reached");
});
test("repeating plan persistence", async ({ page }) => {
await page.goto("/");
const tomorrow = getWeekday(1);
let lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
let modal = await page.getByTestId("charging-plan-modal");
await modal.getByRole("button", { name: "Add repeating plan" }).click();
const plan = modal.getByTestId("plan-entry").nth(1);
await plan.getByTestId("repeating-plan-weekdays").click();
await plan.getByRole("checkbox", { name: "Select all" }).click(); // check all
await plan.getByRole("checkbox", { name: "Select all" }).click(); // uncheck all
await plan.getByRole("checkbox", { name: tomorrow }).check();
await plan.getByTestId("repeating-plan-time").fill("09:20");
await plan.getByTestId("repeating-plan-precondition-lg-toggle").click();
await plan
.getByTestId("repeating-plan-precondition-lg-select")
.getByRole("combobox")
.selectOption("2 hours");
await plan.getByTestId("repeating-plan-active").click();
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("09:20");
await restart(CONFIG);
await page.goto("/");
lp1 = await page.getByTestId("loadpoint").first();
await lp1
.getByTestId("change-vehicle")
.locator("select")
.selectOption("Vehicle with SoC with Capacity");
await lp1
.getByTestId("charging-plan")
.getByRole("button", { name: "tomorrow 09:20" })
.click();
modal = await page.getByTestId("charging-plan-modal");
await expect(modal.getByTestId("plan-entry")).toHaveCount(2);
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
await expect(modal.getByTestId("target-text")).toContainText("09:20");
await expect(modal.getByTestId("repeating-plan-precondition-lg-toggle")).toBeChecked();
await expect(
modal.getByTestId("repeating-plan-precondition-lg-select").locator("option:checked")
).toHaveText("2 hours");
});
});
// add test for precondition, start with basic.evcc.yaml and verify that precondition toggle element is not visible. make dedicated describe block
test.describe("precondition", async () => {
test("only if dynamic tariff exists", async ({ page }) => {
await restart(CONFIG_NO_TARIFF);
await page.goto("/");
const lp1 = await page.getByTestId("loadpoint").first();
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await expect(page.getByTestId("static-plan-active")).toBeVisible();
await expect(page.getByTestId("static-plan-precondition-lg-toggle")).not.toBeVisible();
await expect(page.getByTestId("static-plan-precondition-lg-select")).not.toBeVisible();
// verify small viewport
await page.setViewportSize(mobile);
await expect(page.getByTestId("static-plan-precondition-select")).not.toBeVisible();
});
});

View file

@ -1,332 +0,0 @@
import { test, expect, devices } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
test.use({ baseURL: baseUrl() });
const mobile = devices["iPhone 12 Mini"].viewport;
const desktop = devices["Desktop Chrome"].viewport;
test.beforeAll(async () => {
await start("basics.evcc.yaml", "sessions.sql");
});
test.afterAll(async () => {
await stop();
});
async function selectLoadpointFilter(page, value) {
await page
.getByTestId("filter-loadpoint")
.locator("visible=true")
.getByRole("combobox")
.selectOption(value);
}
async function selectVehicleFilter(page, value) {
await page
.getByTestId("filter-vehicle")
.locator("visible=true")
.getByRole("combobox")
.selectOption(value);
}
test.describe("basics", async () => {
test("navigation to sessions", async ({ page }) => {
await page.goto("/");
const topNavigationButton = await page.getByTestId("topnavigation-button");
await topNavigationButton.click();
await page
.getByTestId("topnavigation-dropdown")
.getByRole("link", { name: "Charging sessions" })
.click();
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
});
test("month without data", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=6");
await expect(page.getByTestId("sessions-nodata")).toHaveCount(1);
await expect(page.getByTestId("sessions-entry")).toHaveCount(0);
});
test("month with data", async ({ page }) => {
await page.setViewportSize(desktop);
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("navigate-month")).toHaveText("May");
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
await expect(page.getByRole("table")).toBeVisible();
await expect(page.getByTestId("sessions-head")).toHaveCount(1);
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(9);
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible("20.0");
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible("67.3");
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
await expect(page.getByTestId("sessions-foot-price")).toBeVisible("5.50");
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible("27.5");
await expect(page.getByTestId("sessions-head-chargeDuration")).toContainText("Durationh:mm");
await expect(page.getByTestId("sessions-foot-chargeDuration")).toBeVisible("1:30");
await page
.getByTestId("sessions-head-chargeDuration")
.getByRole("combobox")
.selectOption("⌀ Power");
await expect(page.getByTestId("sessions-head-avgPower")).toContainText("⌀ PowerkW");
await expect(page.getByTestId("sessions-foot-avgPower")).toBeVisible("1:30");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
});
});
test.describe("mobile basics", async () => {
test("column select", async ({ page }) => {
await page.setViewportSize(mobile);
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(5);
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible("20.0");
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible("67.3");
await page.getByTestId("sessions-head-solar").getByRole("combobox").selectOption("Cost");
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
await expect(page.getByTestId("sessions-foot-price")).toBeVisible("5.50");
await page.getByTestId("sessions-head-price").getByRole("combobox").selectOption("⌀ Price");
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible("27.5");
});
test("keep selection when paging", async ({ page }) => {
await page.setViewportSize(mobile);
await page.goto("/#/sessions?year=2023&month=5");
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
await page.getByTestId("navigate-next-year-month").click();
await page.getByTestId("navigate-prev-year-month").click();
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
});
});
test.describe("paging", async () => {
test("prev/next links", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
});
test("next month", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await page.getByTestId("navigate-next-month").click();
await expect(page.getByTestId("navigate-month")).toHaveText("June");
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
});
test("prev month", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=6");
await page.getByTestId("navigate-prev-month").click();
await expect(page.getByTestId("navigate-month")).toHaveText("May");
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
});
});
for (const [name, viewport] of Object.entries({ desktop, mobile })) {
test.describe(`filter on ${name}`, async () => {
test("by vehicle", async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
await selectVehicleFilter(page, "blauer e-Golf (2)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
await selectVehicleFilter(page, "weißes Model 3 (2)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/weißes Model 3/);
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/weißes Model 3/);
await selectVehicleFilter(page, "all vehicles (4)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
});
test("by loadpoint", async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
await selectLoadpointFilter(page, "Carport (3)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
await expect(page.getByTestId("sessions-entry").nth(2)).toHaveText(/Carport/);
await selectLoadpointFilter(page, "Garage (1)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Garage/);
await selectLoadpointFilter(page, "all charging points (4)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
});
test("by vehicle and loadpoint", async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto("/#/sessions?year=2023&month=5");
await selectLoadpointFilter(page, "Carport (3)");
await selectVehicleFilter(page, "weißes Model 3 (1)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
await expect(page.getByTestId("sessions-entry")).toHaveText(/Carport/);
await expect(page.getByTestId("sessions-entry")).toHaveText(/weißes Model 3/);
await selectVehicleFilter(page, "blauer e-Golf (2)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
});
test("by vehicle and loadpoint disabled options", async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto("/#/sessions?year=2023&month=5");
await selectVehicleFilter(page, "blauer e-Golf (2)");
const option = page
.getByTestId("filter-loadpoint")
.locator("visible=true")
.locator("option[value=Garage]");
await expect(option).toHaveAttribute("disabled", "");
await expect(option).toHaveText("Garage (0)");
});
test("keep filter when paging", async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto("/#/sessions?year=2023&month=5");
await selectLoadpointFilter(page, "Carport (3)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
if (name === "mobile") {
await page.getByTestId("navigate-next-year-month").click();
await page.getByTestId("navigate-prev-year-month").click();
} else {
await page.getByTestId("navigate-next-month").click();
await page.getByTestId("navigate-prev-month").click();
}
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
});
});
}
test.describe("columns desktop", async () => {
test("show vehicle column if multiple different exist", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("vehicle")).toBeVisible();
});
test("show loadpoint column if multiple different exist", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("loadpoint")).toBeVisible();
});
test("show co2 column it has values", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=3");
await expect(page.getByTestId("sessions-head-co2")).toBeVisible();
});
test("hide co2 column if it doesnt have values", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("sessions-head-co2")).toHaveCount(0);
});
});
const date = new Date();
const YEAR = date.getFullYear();
const MONTH = date.getMonth() + 1;
const MONTH_YEAR = new Intl.DateTimeFormat("en", { month: "long", year: "numeric" }).format(date);
test.describe("csv export", async () => {
test("total export", async ({ page }) => {
await page.goto("/#/sessions?period=total");
await expect(page.getByRole("link", { name: "Download total CSV" })).toHaveAttribute(
"href",
"./api/sessions?format=csv&lang=en"
);
});
test("year export", async ({ page }) => {
// fixed year
await page.goto("/#/sessions?period=year&year=2023");
await expect(page.getByRole("link", { name: "Download 2023 CSV" })).toHaveAttribute(
"href",
"./api/sessions?format=csv&lang=en&year=2023"
);
// current year
await page.goto(`/#/sessions?period=year`);
await expect(page.getByRole("link", { name: `Download ${YEAR} CSV` })).toHaveAttribute(
"href",
`./api/sessions?format=csv&lang=en&year=${YEAR}`
);
});
test("monthly export", async ({ page }) => {
await page.goto("/#/sessions?&year=2023&month=5");
await expect(page.getByRole("link", { name: "Download May 2023 CSV" })).toHaveAttribute(
"href",
"./api/sessions?format=csv&lang=en&year=2023&month=5"
);
// current month
await page.goto(`/#/sessions`);
await expect(page.getByRole("link", { name: `Download ${MONTH_YEAR} CSV` })).toHaveAttribute(
"href",
`./api/sessions?format=csv&lang=en&year=${YEAR}&month=${MONTH}`
);
});
});
test.describe("session details", async () => {
test("show session details (session 5)", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await page.getByTestId("sessions-entry").nth(0).click();
await expect(page.getByTestId("session-details")).toBeVisible();
await expect(
page.getByTestId("session-details").getByRole("heading", { name: "Charging Session" })
).toBeVisible();
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Garage");
await expect(page.getByTestId("session-details-vehicle")).toContainText("weißes Model 3");
await expect(page.getByTestId("session-details-date")).toContainText(
["Thu, May 4, 22:00", "Fri, May 5, 06:00"].join("")
);
await expect(page.getByTestId("session-details-energy")).toContainText("5.0 kWh");
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
await expect(page.getByTestId("session-details-solar")).toContainText("0.0% (0.0 kWh)");
await expect(page.getByTestId("session-details-price")).toContainText("2.50 € 50.0 ct/kWh");
await expect(page.getByTestId("session-details-co2")).toHaveCount(0);
await expect(page.getByTestId("session-details-odometer")).toHaveCount(0);
await expect(page.getByTestId("session-details-meter")).toHaveCount(0);
await expect(page.getByTestId("session-details-delete")).toContainText("Delete");
});
test("show session details with CO2 data (session 1)", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=3");
await page.getByTestId("sessions-entry").nth(0).click();
await expect(page.getByTestId("session-details")).toBeVisible();
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Carport");
await expect(page.getByTestId("session-details-vehicle")).toContainText("blauer e-Golf");
await expect(page.getByTestId("session-details-date")).toContainText(
["Wed, Mar 1, 07:00", "Tue, May 2, 12:00"].join("")
);
await expect(page.getByTestId("session-details-energy")).toContainText("10.0 kWh");
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
await expect(page.getByTestId("session-details-solar")).toContainText("100.0% (10.0 kWh)");
await expect(page.getByTestId("session-details-price")).toContainText("2.00 € 20.0 ct/kWh");
await expect(page.getByTestId("session-details-co2")).toContainText("300 g/kWh");
});
});

343
tests/sessions.spec.ts Normal file
View file

@ -0,0 +1,343 @@
import { test, expect, devices, type Page } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
test.use({ baseURL: baseUrl() });
const mobile = devices["iPhone 12 Mini"].viewport;
const desktop = devices["Desktop Chrome"].viewport;
test.beforeAll(async () => {
await start("basics.evcc.yaml", "sessions.sql");
});
test.afterAll(async () => {
await stop();
});
async function selectLoadpointFilter(page: Page, value: string) {
await page
.getByTestId("filter-loadpoint")
.locator("visible=true")
.getByRole("combobox")
.selectOption(value);
}
async function selectVehicleFilter(page: Page, value: string) {
await page
.getByTestId("filter-vehicle")
.locator("visible=true")
.getByRole("combobox")
.selectOption(value);
}
test.describe("basics", async () => {
test("navigation to sessions", async ({ page }) => {
await page.goto("/");
const topNavigationButton = await page.getByTestId("topnavigation-button");
await topNavigationButton.click();
await page
.getByTestId("topnavigation-dropdown")
.getByRole("link", { name: "Charging sessions" })
.click();
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
});
test("month without data", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=6");
await expect(page.getByTestId("sessions-nodata")).toHaveCount(1);
await expect(page.getByTestId("sessions-entry")).toHaveCount(0);
});
test("month with data", async ({ page }) => {
await page.setViewportSize(desktop);
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("navigate-month")).toHaveText("May");
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
await expect(page.getByRole("table")).toBeVisible();
await expect(page.getByTestId("sessions-head")).toHaveCount(1);
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(9);
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
await expect(page.getByTestId("sessions-foot-energy")).toHaveText("20.0");
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
await expect(page.getByTestId("sessions-foot-solar")).toHaveText("67.3");
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
await expect(page.getByTestId("sessions-foot-price")).toBeVisible();
await expect(page.getByTestId("sessions-foot-price")).toHaveText("5.50");
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible();
await expect(page.getByTestId("sessions-foot-avgPrice")).toHaveText("27.5");
await expect(page.getByTestId("sessions-head-chargeDuration")).toContainText(
"Durationh:mm"
);
await expect(page.getByTestId("sessions-foot-chargeDuration")).toBeVisible();
await expect(page.getByTestId("sessions-foot-chargeDuration")).toHaveText("1:30");
await page
.getByTestId("sessions-head-chargeDuration")
.getByRole("combobox")
.selectOption("⌀ Power");
await expect(page.getByTestId("sessions-head-avgPower")).toContainText("⌀ PowerkW");
await expect(page.getByTestId("sessions-foot-avgPower")).toBeVisible();
await expect(page.getByTestId("sessions-foot-avgPower")).toHaveText("10.0");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
});
});
test.describe("mobile basics", async () => {
test("column select", async ({ page }) => {
await page.setViewportSize(mobile);
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(5);
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
await expect(page.getByTestId("sessions-foot-energy")).toHaveText("20.0");
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
await expect(page.getByTestId("sessions-foot-solar")).toHaveText("67.3");
await page.getByTestId("sessions-head-solar").getByRole("combobox").selectOption("Cost");
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
await expect(page.getByTestId("sessions-foot-price")).toBeVisible();
await expect(page.getByTestId("sessions-foot-price")).toHaveText("5.50");
await page.getByTestId("sessions-head-price").getByRole("combobox").selectOption("⌀ Price");
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible();
await expect(page.getByTestId("sessions-foot-avgPrice")).toHaveText("27.5");
});
test("keep selection when paging", async ({ page }) => {
await page.setViewportSize(mobile);
await page.goto("/#/sessions?year=2023&month=5");
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
await page.getByTestId("navigate-next-year-month").click();
await page.getByTestId("navigate-prev-year-month").click();
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
});
});
test.describe("paging", async () => {
test("prev/next links", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
});
test("next month", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await page.getByTestId("navigate-next-month").click();
await expect(page.getByTestId("navigate-month")).toHaveText("June");
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
});
test("prev month", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=6");
await page.getByTestId("navigate-prev-month").click();
await expect(page.getByTestId("navigate-month")).toHaveText("May");
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
});
});
for (const [name, viewport] of Object.entries({ desktop, mobile })) {
test.describe(`filter on ${name}`, async () => {
test("by vehicle", async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
await selectVehicleFilter(page, "blauer e-Golf (2)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
await selectVehicleFilter(page, "weißes Model 3 (2)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/weißes Model 3/);
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/weißes Model 3/);
await selectVehicleFilter(page, "all vehicles (4)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
});
test("by loadpoint", async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
await selectLoadpointFilter(page, "Carport (3)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
await expect(page.getByTestId("sessions-entry").nth(2)).toHaveText(/Carport/);
await selectLoadpointFilter(page, "Garage (1)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Garage/);
await selectLoadpointFilter(page, "all charging points (4)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
});
test("by vehicle and loadpoint", async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto("/#/sessions?year=2023&month=5");
await selectLoadpointFilter(page, "Carport (3)");
await selectVehicleFilter(page, "weißes Model 3 (1)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
await expect(page.getByTestId("sessions-entry")).toHaveText(/Carport/);
await expect(page.getByTestId("sessions-entry")).toHaveText(/weißes Model 3/);
await selectVehicleFilter(page, "blauer e-Golf (2)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
});
test("by vehicle and loadpoint disabled options", async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto("/#/sessions?year=2023&month=5");
await selectVehicleFilter(page, "blauer e-Golf (2)");
const option = page
.getByTestId("filter-loadpoint")
.locator("visible=true")
.locator("option[value=Garage]");
await expect(option).toHaveAttribute("disabled", "");
await expect(option).toHaveText("Garage (0)");
});
test("keep filter when paging", async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto("/#/sessions?year=2023&month=5");
await selectLoadpointFilter(page, "Carport (3)");
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
if (name === "mobile") {
await page.getByTestId("navigate-next-year-month").click();
await page.getByTestId("navigate-prev-year-month").click();
} else {
await page.getByTestId("navigate-next-month").click();
await page.getByTestId("navigate-prev-month").click();
}
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
});
});
}
test.describe("columns desktop", async () => {
test("show vehicle column if multiple different exist", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("vehicle")).toBeVisible();
});
test("show loadpoint column if multiple different exist", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("loadpoint")).toBeVisible();
});
test("show co2 column it has values", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=3");
await expect(page.getByTestId("sessions-head-co2")).toBeVisible();
});
test("hide co2 column if it doesnt have values", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await expect(page.getByTestId("sessions-head-co2")).toHaveCount(0);
});
});
const date = new Date();
const YEAR = date.getFullYear();
const MONTH = date.getMonth() + 1;
const MONTH_YEAR = new Intl.DateTimeFormat("en", { month: "long", year: "numeric" }).format(date);
test.describe("csv export", async () => {
test("total export", async ({ page }) => {
await page.goto("/#/sessions?period=total");
await expect(page.getByRole("link", { name: "Download total CSV" })).toHaveAttribute(
"href",
"./api/sessions?format=csv&lang=en"
);
});
test("year export", async ({ page }) => {
// fixed year
await page.goto("/#/sessions?period=year&year=2023");
await expect(page.getByRole("link", { name: "Download 2023 CSV" })).toHaveAttribute(
"href",
"./api/sessions?format=csv&lang=en&year=2023"
);
// current year
await page.goto(`/#/sessions?period=year`);
await expect(page.getByRole("link", { name: `Download ${YEAR} CSV` })).toHaveAttribute(
"href",
`./api/sessions?format=csv&lang=en&year=${YEAR}`
);
});
test("monthly export", async ({ page }) => {
await page.goto("/#/sessions?&year=2023&month=5");
await expect(page.getByRole("link", { name: "Download May 2023 CSV" })).toHaveAttribute(
"href",
"./api/sessions?format=csv&lang=en&year=2023&month=5"
);
// current month
await page.goto(`/#/sessions`);
await expect(
page.getByRole("link", { name: `Download ${MONTH_YEAR} CSV` })
).toHaveAttribute("href", `./api/sessions?format=csv&lang=en&year=${YEAR}&month=${MONTH}`);
});
});
test.describe("session details", async () => {
test("show session details (session 5)", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=5");
await page.getByTestId("sessions-entry").nth(0).click();
await expect(page.getByTestId("session-details")).toBeVisible();
await expect(
page.getByTestId("session-details").getByRole("heading", { name: "Charging Session" })
).toBeVisible();
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Garage");
await expect(page.getByTestId("session-details-vehicle")).toContainText("weißes Model 3");
await expect(page.getByTestId("session-details-date")).toContainText(
["Thu, May 4, 22:00", "Fri, May 5, 06:00"].join("")
);
await expect(page.getByTestId("session-details-energy")).toContainText("5.0 kWh");
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
await expect(page.getByTestId("session-details-solar")).toContainText("0.0% (0.0 kWh)");
await expect(page.getByTestId("session-details-price")).toContainText("2.50 € 50.0 ct/kWh");
await expect(page.getByTestId("session-details-co2")).toHaveCount(0);
await expect(page.getByTestId("session-details-odometer")).toHaveCount(0);
await expect(page.getByTestId("session-details-meter")).toHaveCount(0);
await expect(page.getByTestId("session-details-delete")).toContainText("Delete");
});
test("show session details with CO2 data (session 1)", async ({ page }) => {
await page.goto("/#/sessions?year=2023&month=3");
await page.getByTestId("sessions-entry").nth(0).click();
await expect(page.getByTestId("session-details")).toBeVisible();
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Carport");
await expect(page.getByTestId("session-details-vehicle")).toContainText("blauer e-Golf");
await expect(page.getByTestId("session-details-date")).toContainText(
["Wed, Mar 1, 07:00", "Tue, May 2, 12:00"].join("")
);
await expect(page.getByTestId("session-details-energy")).toContainText("10.0 kWh");
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
await expect(page.getByTestId("session-details-solar")).toContainText("100.0% (10.0 kWh)");
await expect(page.getByTestId("session-details-price")).toContainText("2.00 € 20.0 ct/kWh");
await expect(page.getByTestId("session-details-co2")).toContainText("300 g/kWh");
});
});

View file

@ -1,77 +0,0 @@
import os from "os";
import path from "path";
import fs from "fs";
import waitOn from "wait-on";
import axios from "axios";
import { spawn } from "child_process";
import { Transform } from "stream";
function workerPort() {
const index = process.env.TEST_WORKER_INDEX * 1;
return 12000 + index;
}
function logPrefix() {
return `[worker:${process.env.TEST_WORKER_INDEX}]`;
}
function createSteamLog() {
return new Transform({
transform(chunk, encoding, callback) {
const lines = chunk.toString().split("\n");
lines.forEach((line) => {
if (line.trim()) log(line);
});
callback();
},
});
}
function log(...args) {
console.log(logPrefix(), ...args);
}
export function simulatorHost() {
return `localhost:${workerPort()}`;
}
export function simulatorUrl() {
return `http://${simulatorHost()}`;
}
export function simulatorConfig() {
const input = "./tests/simulator.evcc.yaml";
const content = fs.readFileSync(input, "utf8");
const result = content.replace(/localhost:7072/g, simulatorHost());
const resultName = "simulator.evcc.generated.yaml";
const resultPath = path.join(os.tmpdir(), resultName);
fs.writeFileSync(resultPath, result);
return resultPath;
}
export async function startSimulator() {
const port = workerPort();
log("starting simulator", { port });
log(`wait until port ${port} is available`);
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
const instance = spawn("npm", ["run", "simulator", "--", "--port", port]);
const steamLog = createSteamLog();
instance.stdout.pipe(steamLog);
instance.stderr.pipe(steamLog);
instance.on("exit", (code) => {
log("simulator terminated", { code, port });
steamLog.end();
});
await waitOn({ resources: [`${simulatorUrl()}/api/state`], log: true });
}
export async function stopSimulator() {
const port = workerPort();
log("shutting down simulator", { port });
await axios.post(`${simulatorUrl()}/api/shutdown`);
log(`wait until port ${port} is closed`);
await waitOn({ resources: [`tcp:localhost:${port}`], reverse: true });
}

77
tests/simulator.ts Normal file
View file

@ -0,0 +1,77 @@
import os from "os";
import path from "path";
import fs from "fs";
import waitOn from "wait-on";
import axios from "axios";
import { spawn } from "child_process";
import { Transform } from "stream";
function workerPort() {
const index = parseInt(process.env["TEST_WORKER_INDEX"] ?? "-1");
return 12000 + index;
}
function logPrefix() {
return `[worker:${process.env["TEST_WORKER_INDEX"]}]`;
}
function createSteamLog() {
return new Transform({
transform(chunk: Buffer, _, callback) {
const lines = chunk.toString().split("\n");
lines.forEach((line) => {
if (line.trim()) log(line);
});
callback();
},
});
}
function log(...args: any[]) {
console.log(logPrefix(), ...args);
}
export function simulatorHost() {
return `localhost:${workerPort()}`;
}
export function simulatorUrl() {
return `http://${simulatorHost()}`;
}
export function simulatorConfig() {
const input = "./tests/simulator.evcc.yaml";
const content = fs.readFileSync(input, "utf8");
const result = content.replace(/localhost:7072/g, simulatorHost());
const resultName = "simulator.evcc.generated.yaml";
const resultPath = path.join(os.tmpdir(), resultName);
fs.writeFileSync(resultPath, result);
return resultPath;
}
export async function startSimulator() {
const port = workerPort();
log("starting simulator", { port });
log(`wait until port ${port} is available`);
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
const instance = spawn("npm", ["run", "simulator", "--", "--port", port.toString()]);
const steamLog = createSteamLog();
instance.stdout.pipe(steamLog);
instance.stderr.pipe(steamLog);
instance.on("exit", (code) => {
log("simulator terminated", { code, port });
steamLog.end();
});
await waitOn({ resources: [`${simulatorUrl()}/api/state`], log: true });
}
export async function stopSimulator() {
const port = workerPort();
log("shutting down simulator", { port });
await axios.post(`${simulatorUrl()}/api/shutdown`);
log(`wait until port ${port} is closed`);
await waitOn({ resources: [`tcp:localhost:${port}`], reverse: true });
}

View file

@ -1,103 +0,0 @@
import bodyParser from "body-parser";
let state = {
site: {
grid: { power: 0 },
pv: { power: 0 },
battery: { power: 0, soc: 0 },
},
loadpoints: [{ power: 0, energy: 0, enabled: false, status: "A" }],
vehicles: [{ soc: 0, range: 0 }],
};
const loggingMiddleware = (req, res, next) => {
console.log(`[simulator] ${req.method} ${req.originalUrl}`);
next();
};
const stateApiMiddleware = (req, res, next) => {
if (req.method === "POST" && req.originalUrl === "/api/state") {
console.log("[simulator] POST /api/state");
state = req.body;
res.end();
} else if (req.method === "POST" && req.originalUrl === "/api/shutdown") {
console.log("[simulator] POST /api/shutdown");
res.end();
process.exit();
} else if (req.originalUrl === "/api/state") {
res.end(JSON.stringify(state));
} else {
next();
}
};
const openemsMiddleware = (req, res, next) => {
const endpoints = {
"/rest/channel/_sum/GridActivePower": { value: state.site.grid.power },
"/rest/channel/_sum/ProductionActivePower": { value: state.site.pv.power },
"/rest/channel/_sum/EssDischargePower": { value: state.site.battery.power },
"/rest/channel/_sum/EssSoc": { value: state.site.battery.soc },
};
const endpoint = endpoints[req.originalUrl];
if (req.method === "GET" && endpoint) {
console.log("[simulator] GET", req.originalUrl);
res.end(JSON.stringify(endpoint));
} else {
next();
}
};
const teslaloggerMiddleware = (req, res, next) => {
if (req.method === "GET" && req.originalUrl.startsWith("/currentjson/")) {
const id = parseInt(req.originalUrl.split("/")[2]);
const vehicle = state.vehicles[id - 1];
if (!vehicle) {
res.statusCode = 404;
res.end(JSON.stringify({ error: "Vehicle not found" }));
return;
}
const data = {
battery_level: vehicle.soc,
battery_range_km: vehicle.range,
plugged_in: true,
charging: false,
odometer: 10000,
is_preconditioning: false,
charge_current_request: 10,
};
res.end(JSON.stringify(data));
} else {
next();
}
};
const shellyMiddleware = (req, res, next) => {
// simulate a shelly gen2 switch device api. implement power and energy
if (req.originalUrl === "/shelly") {
res.end(JSON.stringify({ gen: 2 }));
} else if (req.originalUrl === "/rpc/Shelly.ListMethods") {
res.end(JSON.stringify({ methods: ["Switch.GetStatus"] }));
} else if (req.originalUrl === "/rpc/Switch.GetStatus?id=0") {
res.end(
JSON.stringify({ apower: state.site.pv.power, aenergy: { total: state.site.pv.energy } })
);
} else {
next();
}
};
export default () => ({
name: "api",
enforce: "pre",
configureServer(server) {
console.log("[simulator] configured");
return () => {
server.middlewares.use(loggingMiddleware);
server.middlewares.use(bodyParser.json());
server.middlewares.use(stateApiMiddleware);
server.middlewares.use(openemsMiddleware);
server.middlewares.use(teslaloggerMiddleware);
server.middlewares.use(shellyMiddleware);
};
},
});

129
tests/simulator/api.ts Normal file
View file

@ -0,0 +1,129 @@
import bodyParser from "body-parser";
import type { Connect, ViteDevServer } from "vite";
import type { ServerResponse } from "http";
let state = {
site: {
grid: { power: 0 },
pv: { power: 0, energy: 0 },
battery: { power: 0, soc: 0 },
},
loadpoints: [{ power: 0, energy: 0, enabled: false, status: "A" }],
vehicles: [{ soc: 0, range: 0 }],
};
const loggingMiddleware = (
req: Connect.IncomingMessage,
_: ServerResponse,
next: Connect.NextFunction
) => {
console.log(`[simulator] ${req.method} ${req.url}`);
next();
};
const stateApiMiddleware = (
req: Connect.IncomingMessage,
res: ServerResponse,
next: Connect.NextFunction
) => {
if (req.method === "POST" && req.originalUrl === "/api/state") {
console.log("[simulator] POST /api/state");
// @ts-expect-error Property 'body' does not exist on type 'IncomingMessage'
state = req.body;
res.end();
} else if (req.method === "POST" && req.originalUrl === "/api/shutdown") {
console.log("[simulator] POST /api/shutdown");
res.end();
process.exit();
} else if (req.originalUrl === "/api/state") {
res.end(JSON.stringify(state));
} else {
next();
}
};
const openemsMiddleware = (
req: Connect.IncomingMessage,
res: ServerResponse,
next: Connect.NextFunction
) => {
const endpoints = {
"/rest/channel/_sum/GridActivePower": { value: state.site.grid.power },
"/rest/channel/_sum/ProductionActivePower": { value: state.site.pv.power },
"/rest/channel/_sum/EssDischargePower": { value: state.site.battery.power },
"/rest/channel/_sum/EssSoc": { value: state.site.battery.soc },
};
const endpoint = endpoints[req.originalUrl as keyof typeof endpoints];
if (req.method === "GET" && endpoint) {
console.log("[simulator] GET", req.originalUrl);
res.end(JSON.stringify(endpoint));
} else {
next();
}
};
const teslaloggerMiddleware = (
req: Connect.IncomingMessage,
res: ServerResponse,
next: Connect.NextFunction
) => {
if (req.method === "GET" && req.originalUrl && req.originalUrl.startsWith("/currentjson/")) {
const id = parseInt(req.originalUrl.split("/")[2]);
const vehicle = state.vehicles[id - 1];
if (!vehicle) {
res.statusCode = 404;
res.end(JSON.stringify({ error: "Vehicle not found" }));
return;
}
const data = {
battery_level: vehicle.soc,
battery_range_km: vehicle.range,
plugged_in: true,
charging: false,
odometer: 10000,
is_preconditioning: false,
charge_current_request: 10,
};
res.end(JSON.stringify(data));
} else {
next();
}
};
const shellyMiddleware = (
req: Connect.IncomingMessage,
res: ServerResponse,
next: Connect.NextFunction
) => {
// simulate a shelly gen2 switch device api. implement power and energy
if (req.originalUrl === "/shelly") {
res.end(JSON.stringify({ gen: 2 }));
} else if (req.originalUrl === "/rpc/Shelly.ListMethods") {
res.end(JSON.stringify({ methods: ["Switch.GetStatus"] }));
} else if (req.originalUrl === "/rpc/Switch.GetStatus?id=0") {
res.end(
JSON.stringify({
apower: state.site.pv.power,
aenergy: { total: state.site.pv.energy },
})
);
} else {
next();
}
};
export default () => ({
name: "api",
enforce: "pre",
configureServer(server: ViteDevServer) {
console.log("[simulator] configured");
return () => {
server.middlewares.use(loggingMiddleware);
server.middlewares.use(bodyParser.json());
server.middlewares.use(stateApiMiddleware);
server.middlewares.use(openemsMiddleware);
server.middlewares.use(teslaloggerMiddleware);
server.middlewares.use(shellyMiddleware);
};
},
});

View file

@ -239,14 +239,28 @@
</form>
</template>
<script>
<script lang="ts">
import axios from "axios";
import { defineComponent } from "vue";
export default {
export default defineComponent({
name: "Simulator",
data() {
return {
state: null,
state: null as {
site: {
grid: { power: number };
pv: { power: number; energy: number };
battery: { power: number; soc: number };
};
loadpoints: {
power: number;
energy: number;
enabled: boolean;
status: string;
}[];
vehicles: { soc: number; range: number }[];
} | null,
};
},
mounted() {
@ -262,24 +276,24 @@ export default {
},
addVehicle() {
// push a duplacate of the last entry
this.state.vehicles.push({
this.state?.vehicles.push({
...this.state.vehicles[this.state.vehicles.length - 1],
});
},
removeVehicle(index) {
this.state.vehicles.splice(index, 1);
removeVehicle(index: number) {
this.state?.vehicles.splice(index, 1);
},
addLoadpoint() {
// push a duplacate of the last entry
this.state.loadpoints.push({
this.state?.loadpoints.push({
...this.state.loadpoints[this.state.loadpoints.length - 1],
});
},
removeLoadpoint(index) {
this.state.loadpoints.splice(index, 1);
removeLoadpoint(index: number) {
this.state?.loadpoints.splice(index, 1);
},
},
};
});
</script>
<style>

View file

@ -1,8 +0,0 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import api from "./api";
export default defineConfig({
plugins: [vue(), api()],
server: { port: 7072, host: true },
});

View file

@ -0,0 +1,12 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import api from "./api";
export default defineConfig({
plugins: [
vue(),
// @ts-expect-error the in api() declared 'enforce' property is not supported in TypeScript but in JavaScript
api(),
],
server: { port: 7072, host: true },
});

View file

@ -1,4 +1,4 @@
import { expect, Page, Locator } from "@playwright/test";
import { expect, type Page, type Locator } from "@playwright/test";
export async function enableExperimental(page: Page): Promise<void> {
await page.getByTestId("topnavigation-button").click();

View file

@ -1,6 +1,6 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "assets/js/**/*"],
"include": ["env.d.ts", "assets/js/**/*", "tests/**/*"],
"compilerOptions": {
"allowUnreachableCode": false,
"allowUnusedLabels": false,