diff --git a/assets/js/store.ts b/assets/js/store.ts
index d659080df..d112ce02a 100644
--- a/assets/js/store.ts
+++ b/assets/js/store.ts
@@ -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);
diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts
index 000873e0c..203af9cec 100644
--- a/assets/js/types/evcc.ts
+++ b/assets/js/types/evcc.ts
@@ -31,6 +31,7 @@ export interface State {
currency?: CURRENCY;
fatal?: FatalError;
auth?: Auth;
+ vehicles: Vehicle[];
}
export interface LoadpointCompact {
diff --git a/package-lock.json b/package-lock.json
index f642759e4..4538986ba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index aeca7a9ce..5358f6d5e 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/tests/auth.spec.js b/tests/auth.spec.ts
similarity index 100%
rename from tests/auth.spec.js
rename to tests/auth.spec.ts
diff --git a/tests/basics.spec.js b/tests/basics.spec.ts
similarity index 98%
rename from tests/basics.spec.js
rename to tests/basics.spec.ts
index e77fc315b..98a8fc152 100644
--- a/tests/basics.spec.js
+++ b/tests/basics.spec.ts
@@ -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();
diff --git a/tests/battery-settings.spec.js b/tests/battery-settings.spec.ts
similarity index 100%
rename from tests/battery-settings.spec.js
rename to tests/battery-settings.spec.ts
diff --git a/tests/boot.spec.js b/tests/boot.spec.ts
similarity index 100%
rename from tests/boot.spec.js
rename to tests/boot.spec.ts
diff --git a/tests/config-aux.spec.js b/tests/config-aux.spec.ts
similarity index 98%
rename from tests/config-aux.spec.js
rename to tests/config-aux.spec.ts
index d8cf0b88e..edf6c26c2 100644
--- a/tests/config-aux.spec.js
+++ b/tests/config-aux.spec.ts
@@ -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");
diff --git a/tests/config-battery.spec.js b/tests/config-battery.spec.js
deleted file mode 100644
index 9dfe0bf38..000000000
--- a/tests/config-battery.spec.js
+++ /dev/null
@@ -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();
- });
-});
diff --git a/tests/config-battery.spec.ts b/tests/config-battery.spec.ts
new file mode 100644
index 000000000..04fdf4a63
--- /dev/null
+++ b/tests/config-battery.spec.ts
@@ -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();
+ });
+});
diff --git a/tests/config-circuit.spec.js b/tests/config-circuit.spec.ts
similarity index 100%
rename from tests/config-circuit.spec.js
rename to tests/config-circuit.spec.ts
diff --git a/tests/config-grid.spec.js b/tests/config-grid.spec.ts
similarity index 100%
rename from tests/config-grid.spec.js
rename to tests/config-grid.spec.ts
diff --git a/tests/config-loadpoint.spec.js b/tests/config-loadpoint.spec.js
deleted file mode 100644
index 5357643b3..000000000
--- a/tests/config-loadpoint.spec.js
+++ /dev/null
@@ -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");
- });
-});
diff --git a/tests/config-loadpoint.spec.ts b/tests/config-loadpoint.spec.ts
new file mode 100644
index 000000000..81cc4883e
--- /dev/null
+++ b/tests/config-loadpoint.spec.ts
@@ -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");
+ });
+});
diff --git a/tests/config-messaging.spec.js b/tests/config-messaging.spec.js
deleted file mode 100644
index c110a83d5..000000000
--- a/tests/config-messaging.spec.js
+++ /dev/null
@@ -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");
- });
-});
diff --git a/tests/config-messaging.spec.ts b/tests/config-messaging.spec.ts
new file mode 100644
index 000000000..05ef0c74e
--- /dev/null
+++ b/tests/config-messaging.spec.ts
@@ -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");
+ });
+});
diff --git a/tests/config-mqtt.spec.js b/tests/config-mqtt.spec.ts
similarity index 95%
rename from tests/config-mqtt.spec.js
rename to tests/config-mqtt.spec.ts
index d17180d01..bebff86c6 100644
--- a/tests/config-mqtt.spec.js
+++ b/tests/config-mqtt.spec.ts
@@ -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`
);
diff --git a/tests/config-onboarding.spec.js b/tests/config-onboarding.spec.ts
similarity index 100%
rename from tests/config-onboarding.spec.js
rename to tests/config-onboarding.spec.ts
diff --git a/tests/config-pv.spec.js b/tests/config-pv.spec.js
deleted file mode 100644
index 1f079afdc..000000000
--- a/tests/config-pv.spec.js
+++ /dev/null
@@ -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();
- });
-});
diff --git a/tests/config-pv.spec.ts b/tests/config-pv.spec.ts
new file mode 100644
index 000000000..2be2cb3d2
--- /dev/null
+++ b/tests/config-pv.spec.ts
@@ -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();
+ });
+});
diff --git a/tests/config-tariffs.spec.js b/tests/config-tariffs.spec.js
deleted file mode 100644
index aaf72b1e0..000000000
--- a/tests/config-tariffs.spec.js
+++ /dev/null
@@ -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("")
- );
- });
-});
diff --git a/tests/config-tariffs.spec.ts b/tests/config-tariffs.spec.ts
new file mode 100644
index 000000000..0b767aa2d
--- /dev/null
+++ b/tests/config-tariffs.spec.ts
@@ -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("")
+ );
+ });
+});
diff --git a/tests/config-vehicles.spec.js b/tests/config-vehicles.spec.ts
similarity index 100%
rename from tests/config-vehicles.spec.js
rename to tests/config-vehicles.spec.ts
diff --git a/tests/config.spec.js b/tests/config.spec.ts
similarity index 100%
rename from tests/config.spec.js
rename to tests/config.spec.ts
diff --git a/tests/custom-css.spec.js b/tests/custom-css.spec.ts
similarity index 100%
rename from tests/custom-css.spec.js
rename to tests/custom-css.spec.ts
diff --git a/tests/evcc.js b/tests/evcc.js
deleted file mode 100644
index da8423471..000000000
--- a/tests/evcc.js
+++ /dev/null
@@ -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);
- }
-}
diff --git a/tests/evcc.ts b/tests/evcc.ts
new file mode 100644
index 000000000..d0cf15bc0
--- /dev/null
+++ b/tests/evcc.ts
@@ -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);
+ }
+}
diff --git a/tests/fatal.spec.js b/tests/fatal.spec.ts
similarity index 100%
rename from tests/fatal.spec.js
rename to tests/fatal.spec.ts
diff --git a/tests/heating.spec.js b/tests/heating.spec.ts
similarity index 100%
rename from tests/heating.spec.js
rename to tests/heating.spec.ts
diff --git a/tests/limits.spec.js b/tests/limits.spec.ts
similarity index 100%
rename from tests/limits.spec.js
rename to tests/limits.spec.ts
diff --git a/tests/logs.spec.js b/tests/logs.spec.ts
similarity index 100%
rename from tests/logs.spec.js
rename to tests/logs.spec.ts
diff --git a/tests/modals.spec.js b/tests/modals.spec.ts
similarity index 100%
rename from tests/modals.spec.js
rename to tests/modals.spec.ts
diff --git a/tests/plan.spec.js b/tests/plan.spec.js
deleted file mode 100644
index 94de8e99f..000000000
--- a/tests/plan.spec.js
+++ /dev/null
@@ -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();
- });
-});
diff --git a/tests/plan.spec.ts b/tests/plan.spec.ts
new file mode 100644
index 000000000..d8001021b
--- /dev/null
+++ b/tests/plan.spec.ts
@@ -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();
+ });
+});
diff --git a/tests/sessions.spec.js b/tests/sessions.spec.js
deleted file mode 100644
index e7cc2af71..000000000
--- a/tests/sessions.spec.js
+++ /dev/null
@@ -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");
- });
-});
diff --git a/tests/sessions.spec.ts b/tests/sessions.spec.ts
new file mode 100644
index 000000000..15beb4323
--- /dev/null
+++ b/tests/sessions.spec.ts
@@ -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");
+ });
+});
diff --git a/tests/simulator.js b/tests/simulator.js
deleted file mode 100644
index 67bcfd527..000000000
--- a/tests/simulator.js
+++ /dev/null
@@ -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 });
-}
diff --git a/tests/simulator.ts b/tests/simulator.ts
new file mode 100644
index 000000000..0185e3e34
--- /dev/null
+++ b/tests/simulator.ts
@@ -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 });
+}
diff --git a/tests/simulator/api.js b/tests/simulator/api.js
deleted file mode 100644
index ae0fb31de..000000000
--- a/tests/simulator/api.js
+++ /dev/null
@@ -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);
- };
- },
-});
diff --git a/tests/simulator/api.ts b/tests/simulator/api.ts
new file mode 100644
index 000000000..7ff330c06
--- /dev/null
+++ b/tests/simulator/api.ts
@@ -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);
+ };
+ },
+});
diff --git a/tests/simulator/src/Simulator.vue b/tests/simulator/src/Simulator.vue
index 0b647c878..045849869 100644
--- a/tests/simulator/src/Simulator.vue
+++ b/tests/simulator/src/Simulator.vue
@@ -239,14 +239,28 @@
-