chore: Typescript: Rewrite tests (#21723)
Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
parent
fda3b066db
commit
d43ab9fa6a
53 changed files with 2287 additions and 2154 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface State {
|
|||
currency?: CURRENCY;
|
||||
fatal?: FatalError;
|
||||
auth?: Auth;
|
||||
vehicles: Vehicle[];
|
||||
}
|
||||
|
||||
export interface LoadpointCompact {
|
||||
|
|
|
|||
33
package-lock.json
generated
33
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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");
|
||||
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
89
tests/config-battery.spec.ts
Normal file
89
tests/config-battery.spec.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("battery meter", async () => {
|
||||
test("create, edit and remove battery meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("battery")).toHaveCount(0);
|
||||
|
||||
// create #1
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add battery meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("Demo Battery");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
|
||||
await meterModal.getByLabel("Power").fill("4000");
|
||||
await meterModal.getByLabel("Charge").fill("80");
|
||||
await expect(meterModal.getByLabel("Maximum AC power of the hybrid inverter")).toHaveCount(
|
||||
0
|
||||
);
|
||||
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
||||
await meterModal.getByRole("link", { name: "validate" }).click();
|
||||
await expect(meterModal.getByTestId("device-tag-power")).toContainText("4.0 kW");
|
||||
await expect(meterModal.getByTestId("device-tag-soc")).toContainText("80.0%");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("battery")).toBeVisible();
|
||||
await expect(page.getByTestId("battery")).toContainText("Demo Battery");
|
||||
|
||||
// edit #1
|
||||
await page.getByTestId("battery").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByLabel("Power").fill("5000");
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
const battery = page.getByTestId("battery");
|
||||
await expect(battery).toBeVisible();
|
||||
await expect(battery).toContainText("Demo Battery");
|
||||
await expect(battery.getByTestId("device-tag-power")).toContainText("5.0 kW");
|
||||
await expect(battery.getByTestId("device-tag-soc")).toContainText("80.0%");
|
||||
|
||||
// restart and check in main ui
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("visualization").click();
|
||||
await expect(page.getByTestId("energyflow")).toContainText("Battery discharging80%5.0 kW");
|
||||
|
||||
// delete #1
|
||||
await page.goto("/#/config");
|
||||
await page.getByTestId("battery").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("battery")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("advanced fields", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add battery meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("Demo Battery");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo battery");
|
||||
|
||||
await expect(meterModal.getByLabel("Meter reading")).not.toBeVisible();
|
||||
await page.getByRole("button", { name: "Show advanced settings" }).click();
|
||||
await expect(meterModal.getByLabel("Meter reading")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Hide advanced settings" }).click();
|
||||
await expect(meterModal.getByLabel("Meter reading")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
512
tests/config-loadpoint.spec.ts
Normal file
512
tests/config-loadpoint.spec.ts
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { expectModalVisible, expectModalHidden, editorClear, editorPaste } from "./utils";
|
||||
|
||||
const CONFIG_EMPTY = "config-empty.evcc.yaml";
|
||||
const CONFIG_ONE_LP = "config-one-lp.evcc.yaml";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
async function enableExperimental(page: Page) {
|
||||
await page
|
||||
.getByTestId("generalconfig-experimental")
|
||||
.getByRole("button", { name: "edit" })
|
||||
.click();
|
||||
await page.getByLabel("Experimental 🧪").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
}
|
||||
|
||||
async function addDemoCharger(page: Page) {
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
||||
|
||||
const modal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Manufacturer").selectOption("Demo charger");
|
||||
await modal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await expectModalVisible(lpModal);
|
||||
}
|
||||
|
||||
async function addDemoMeter(page: Page, power = "0") {
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Add dedicated charger meter" }).click();
|
||||
|
||||
const modal = page.getByTestId("meter-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await modal.getByLabel("Power").fill(power);
|
||||
await modal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
await expectModalVisible(lpModal);
|
||||
}
|
||||
|
||||
async function addVehicle(page: Page, title: string) {
|
||||
await page.getByRole("button", { name: "Add vehicle" }).click();
|
||||
const modal = page.getByTestId("vehicle-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Manufacturer").selectOption("Generic vehicle (without API)");
|
||||
await modal.getByLabel("Title").fill(title);
|
||||
await modal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
}
|
||||
|
||||
async function newLoadpoint(page: Page, title: string) {
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await page.getByRole("button", { name: "Add charge point" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Title").fill(title);
|
||||
}
|
||||
|
||||
test.describe("loadpoint", async () => {
|
||||
test("create, update and delete", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
|
||||
|
||||
// new loadpoint
|
||||
await newLoadpoint(page, "Solar Carport");
|
||||
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
||||
|
||||
// add charger
|
||||
await chargerModal.getByLabel("Manufacturer").selectOption("Demo charger");
|
||||
await chargerModal.getByLabel("Charge status").selectOption("C");
|
||||
await chargerModal.getByLabel("Power").fill("11000");
|
||||
await chargerModal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(lpModal.getByLabel("Title")).toHaveValue("Solar Carport");
|
||||
|
||||
// create loadpoint
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport");
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("charging");
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("11.0 kW");
|
||||
|
||||
// restart button appears
|
||||
const restartButton = await page
|
||||
.getByTestId("bottom-banner")
|
||||
.getByRole("button", { name: "Restart" });
|
||||
await expect(restartButton).toBeVisible();
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport");
|
||||
|
||||
// update loadpoint title
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Title").fill("Solar Carport 2");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport 2");
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Solar Carport 2");
|
||||
|
||||
// update loadpoint power
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByTestId("chargerPower-22kw").click();
|
||||
|
||||
// update charger mode
|
||||
await expect(lpModal.getByRole("textbox", { name: "Charger" })).toHaveValue(
|
||||
"Demo charger [db:1]"
|
||||
);
|
||||
await lpModal.getByRole("textbox", { name: "Charger" }).click();
|
||||
await chargerModal.getByLabel("Charge status").selectOption("A");
|
||||
await chargerModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(chargerModal);
|
||||
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("not connected");
|
||||
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByTestId("chargerPower-22kw")).toHaveClass(/active/);
|
||||
await expect(lpModal.getByLabel("Title")).toHaveValue("Solar Carport 2");
|
||||
await lpModal.getByRole("button", { name: "Close" }).click();
|
||||
|
||||
// delete loadpoint
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("priority", async ({ page }) => {
|
||||
await start(CONFIG_ONE_LP);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
|
||||
// loadpoint from yaml
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Carport");
|
||||
|
||||
// add loadpoint via UI
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// two loadpoints
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
|
||||
await expect(page.getByTestId("loadpoint").nth(0)).toContainText("Carport");
|
||||
await expect(page.getByTestId("loadpoint").nth(1)).toContainText("Garage");
|
||||
|
||||
// second loadpoint: increase priority
|
||||
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Priority")).toHaveValue("0 (default)");
|
||||
await lpModal.getByLabel("Priority").selectOption("1");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_ONE_LP);
|
||||
await page.reload();
|
||||
|
||||
// check priorities
|
||||
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Priority")).toHaveValue("1");
|
||||
|
||||
// change back to 0
|
||||
await lpModal.getByLabel("Priority").selectOption("0 (default)");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_ONE_LP);
|
||||
await page.reload();
|
||||
|
||||
// check priorities
|
||||
await page.getByTestId("loadpoint").nth(1).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Priority")).toHaveValue("0 (default)");
|
||||
});
|
||||
|
||||
test("vehicle", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
const LP_1 = "Carport";
|
||||
const LP_2 = "Garage";
|
||||
const VEHICLE_1 = "Green Car";
|
||||
const VEHICLE_2 = "Yellow Van";
|
||||
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
|
||||
// add loadpoint > no vehicle option
|
||||
await newLoadpoint(page, LP_1);
|
||||
await addDemoCharger(page);
|
||||
await expect(lpModal.getByLabel("Default vehicle")).not.toBeVisible();
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// edit loadpoint
|
||||
await page.getByTestId("loadpoint").nth(0).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal).toContainText("No vehicles are configured.");
|
||||
await lpModal.getByRole("button", { name: "Close" }).click();
|
||||
|
||||
// add vehicles
|
||||
for (const title of [VEHICLE_1, VEHICLE_2]) {
|
||||
await addVehicle(page, title);
|
||||
}
|
||||
|
||||
// set vehicle as default for loadpoint 1
|
||||
await page.getByTestId("loadpoint").nth(0).getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByTestId("loadpointPollMode-charging")).toHaveClass(/active/);
|
||||
await expect(
|
||||
lpModal.getByRole("checkbox", { name: "Interpolate charge level" })
|
||||
).toBeChecked();
|
||||
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_1);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// add second loadpoint
|
||||
await newLoadpoint(page, LP_2);
|
||||
await addDemoCharger(page);
|
||||
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_2);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
|
||||
// check loadpoint default vehicles
|
||||
for (const [index, vehicle] of [VEHICLE_1, VEHICLE_2].entries()) {
|
||||
await page
|
||||
.getByTestId("loadpoint")
|
||||
.nth(index)
|
||||
.getByRole("button", { name: "edit" })
|
||||
.click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.locator("#loadpointParamVehicle option:checked")).toHaveText(
|
||||
vehicle
|
||||
);
|
||||
await lpModal.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
}
|
||||
});
|
||||
|
||||
test("keep mode", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add grid meter
|
||||
await page.getByRole("button", { name: "Add grid meter" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await meterModal.getByLabel("Power").fill("-1000");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
// add a loadpoint with dummy charger,
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await newLoadpoint(page, "Carport");
|
||||
await addDemoCharger(page);
|
||||
await lpModal.getByLabel("Default mode").selectOption("---");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
await restart(CONFIG_EMPTY);
|
||||
|
||||
// change on main ui
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("button", { name: "Off" })).toHaveClass(/active/);
|
||||
await page.getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
await expect(page.getByRole("button", { name: "Solar", exact: true })).toHaveClass(
|
||||
/active/
|
||||
);
|
||||
|
||||
// change default mode in config to fast
|
||||
await page.goto("/#/config");
|
||||
// open first loadpoint
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Default mode").selectOption("Fast");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
await restart(CONFIG_EMPTY);
|
||||
|
||||
// check loadpoint mode
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("button", { name: "Fast" })).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
test("delete vehicle references", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add vehicle, add loadpoint, add charger
|
||||
await addVehicle(page, "Porsche");
|
||||
await addVehicle(page, "Tesla");
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByLabel("Default vehicle").selectOption("Porsche");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// delete vehicle
|
||||
await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click();
|
||||
const vehicleModal = page.getByTestId("vehicle-modal");
|
||||
await expectModalVisible(vehicleModal);
|
||||
await vehicleModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(vehicleModal);
|
||||
|
||||
// restart
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
|
||||
// check loadpoint default vehicle
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByLabel("Default vehicle")).toHaveValue("");
|
||||
});
|
||||
|
||||
test("delete charger references", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add loadpoint, add charger
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
// delete charger
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("textbox", { name: "Charger" }).click();
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(chargerModal);
|
||||
await chargerModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(chargerModal);
|
||||
|
||||
// restart without saving loadpoint
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
|
||||
// check loadpoint default vehicle
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
|
||||
await expect(lpModal).toContainText("Configuring a charger is required.");
|
||||
});
|
||||
|
||||
test("delete meter references", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add loadpoint, add charger
|
||||
await newLoadpoint(page, "Garage");
|
||||
await addDemoCharger(page);
|
||||
await addDemoMeter(page, "11000");
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("11.0 kW");
|
||||
|
||||
// delete charger
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("textbox", { name: "Meter" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
// restart without saving loadpoint
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
|
||||
// check loadpoint default vehicle
|
||||
await expect(page.getByTestId("loadpoint")).not.toContainText("11.0 kW");
|
||||
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
|
||||
await expect(
|
||||
lpModal.getByRole("button", { name: "Add dedicated charger meter" })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("user-defined charger", async ({ page }) => {
|
||||
await start(CONFIG_EMPTY);
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// add loadpoint
|
||||
await newLoadpoint(page, "Carport");
|
||||
const lpModal = page.getByTestId("loadpoint-modal");
|
||||
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
||||
|
||||
// add user-defined charger
|
||||
const chargerModal = page.getByTestId("charger-modal");
|
||||
await expectModalVisible(chargerModal);
|
||||
await chargerModal.getByLabel("Manufacturer").selectOption("User-defined device");
|
||||
await page.waitForLoadState("networkidle");
|
||||
const editor = chargerModal.getByTestId("yaml-editor");
|
||||
await expect(editor).toContainText("status: # charger status [A..F]");
|
||||
|
||||
await editorClear(editor, 10);
|
||||
await editorPaste(
|
||||
editor,
|
||||
page,
|
||||
`status:
|
||||
source: const
|
||||
value: 'C'
|
||||
enabled:
|
||||
source: const
|
||||
value: true
|
||||
enable:
|
||||
source: js
|
||||
script: console.log(enable)
|
||||
maxcurrent:
|
||||
source: js
|
||||
script: console.log(maxcurrent)
|
||||
power:
|
||||
source: const
|
||||
value: 11000`
|
||||
);
|
||||
|
||||
const restResult = chargerModal.getByTestId("test-result");
|
||||
await expect(restResult).toContainText("Status: unknown");
|
||||
await restResult.getByRole("link", { name: "validate" }).click();
|
||||
await expect(restResult).toContainText("Status: successful");
|
||||
await expect(restResult).toContainText(["Status", "charging"].join(""));
|
||||
await expect(restResult).toContainText(["Enabled", "yes"].join(""));
|
||||
await expect(restResult).toContainText(["Power", "11.0 kW"].join(""));
|
||||
|
||||
// create
|
||||
await chargerModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(chargerModal);
|
||||
await expectModalVisible(lpModal);
|
||||
await lpModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(lpModal);
|
||||
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await expect(page.getByTestId("loadpoint")).toContainText("Carport");
|
||||
|
||||
// restart evcc
|
||||
await restart(CONFIG_EMPTY);
|
||||
await page.reload();
|
||||
|
||||
const lpEntry = page.getByTestId("loadpoint");
|
||||
await expect(lpEntry).toHaveCount(1);
|
||||
await expect(lpEntry).toContainText("Carport");
|
||||
await expect(lpEntry).toContainText(["Status", "charging"].join(""));
|
||||
await expect(lpEntry).toContainText(["Enabled", "yes"].join(""));
|
||||
await expect(lpEntry).toContainText(["Power", "11.0 kW"].join(""));
|
||||
|
||||
await lpEntry.getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(lpModal);
|
||||
|
||||
await expect(lpModal.getByLabel("Charger").first()).toHaveValue(
|
||||
"User-defined device [db:1]"
|
||||
);
|
||||
await lpModal.getByLabel("Charger").first().click();
|
||||
await expectModalVisible(chargerModal);
|
||||
|
||||
await expect(chargerModal.getByLabel("Manufacturer")).toHaveValue("User-defined device");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(editor).toContainText("value: 'C'");
|
||||
await expect(editor).toContainText("value: 11000");
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
44
tests/config-messaging.spec.ts
Normal file
44
tests/config-messaging.spec.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
const SELECT_ALL = "ControlOrMeta+KeyA";
|
||||
|
||||
async function goToConfig(page: Page) {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
}
|
||||
|
||||
test.describe("messaging", async () => {
|
||||
test("save a comment", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
|
||||
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
|
||||
const modal = await page.getByTestId("messaging-modal");
|
||||
await expectModalVisible(modal);
|
||||
|
||||
await modal.locator(".monaco-editor .view-line").nth(0).click();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await page.keyboard.press(SELECT_ALL, { delay: 10 });
|
||||
await page.keyboard.press("Backspace", { delay: 10 });
|
||||
}
|
||||
await page.keyboard.type("# hello world");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
|
||||
page.reload();
|
||||
|
||||
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(modal);
|
||||
await expect(modal).toContainText("# hello world");
|
||||
});
|
||||
});
|
||||
|
|
@ -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`
|
||||
);
|
||||
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { startSimulator, stopSimulator, simulatorHost } from "./simulator";
|
||||
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("pv meter", async () => {
|
||||
test("create, edit and remove pv meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
|
||||
// create #1
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("PV North");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await meterModal.getByLabel("Power optional").fill("5000");
|
||||
await page.getByRole("button", { name: "Show advanced settings" }).click();
|
||||
await expect(meterModal.getByLabel("Minimum charge")).not.toBeVisible(); // battery usage only
|
||||
await expect(meterModal.getByLabel("Maximum AC power of the hybrid inverter")).toBeVisible(); // pv usage only
|
||||
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
||||
await meterModal.getByRole("link", { name: "validate" }).click();
|
||||
await expect(meterModal.getByTestId("device-tag-power")).toContainText("5.0 kW");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toBeVisible(1);
|
||||
await expect(page.getByTestId("pv")).toContainText("PV North");
|
||||
|
||||
// edit #1
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByLabel("Power optional").fill("6000");
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
const pv = page.getByTestId("pv");
|
||||
await expect(pv).toBeVisible(1);
|
||||
await expect(pv).toContainText("PV North");
|
||||
await expect(pv.getByTestId("device-tag-power")).toContainText("6.0 kW");
|
||||
|
||||
// restart and check in main ui
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("visualization").click();
|
||||
await expect(page.getByTestId("energyflow")).toContainText("Production6.0 kW");
|
||||
|
||||
// delete #1
|
||||
await page.goto("/#/config");
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
|
||||
// restart and check again
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("remove broken pv meter", async ({ page }) => {
|
||||
// setup test data for mock openems api
|
||||
await startSimulator();
|
||||
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// create meter
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("North Roof");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("shelly-1pm");
|
||||
await meterModal.getByLabel("IP address or hostname").fill(simulatorHost());
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toBeVisible(1);
|
||||
await expect(page.getByTestId("pv")).toContainText("North Roof");
|
||||
|
||||
// break meter
|
||||
await stopSimulator();
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
|
||||
// remove meter
|
||||
await expect(page.getByTestId("fatal-error")).toBeVisible();
|
||||
await expect(page.getByTestId("pv")).toBeVisible(1);
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
|
||||
// restart and check again
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
116
tests/config-pv.spec.ts
Normal file
116
tests/config-pv.spec.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { startSimulator, stopSimulator, simulatorHost } from "./simulator";
|
||||
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("pv meter", async () => {
|
||||
test("create, edit and remove pv meter", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
|
||||
// create #1
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("PV North");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
|
||||
await meterModal.getByLabel("Power optional").fill("5000");
|
||||
await page.getByRole("button", { name: "Show advanced settings" }).click();
|
||||
await expect(meterModal.getByLabel("Minimum charge")).not.toBeVisible(); // battery usage only
|
||||
await expect(
|
||||
meterModal.getByLabel("Maximum AC power of the hybrid inverter")
|
||||
).toBeVisible(); // pv usage only
|
||||
await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible();
|
||||
await meterModal.getByRole("link", { name: "validate" }).click();
|
||||
await expect(meterModal.getByTestId("device-tag-power")).toContainText("5.0 kW");
|
||||
await meterModal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toBeVisible();
|
||||
await expect(page.getByTestId("pv")).toContainText("PV North");
|
||||
|
||||
// edit #1
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByLabel("Power optional").fill("6000");
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
|
||||
const pv = page.getByTestId("pv");
|
||||
await expect(pv).toBeVisible();
|
||||
await expect(pv).toContainText("PV North");
|
||||
await expect(pv.getByTestId("device-tag-power")).toContainText("6.0 kW");
|
||||
|
||||
// restart and check in main ui
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("visualization").click();
|
||||
await expect(page.getByTestId("energyflow")).toContainText("Production6.0 kW");
|
||||
|
||||
// delete #1
|
||||
await page.goto("/#/config");
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
|
||||
// restart and check again
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("remove broken pv meter", async ({ page }) => {
|
||||
// setup test data for mock openems api
|
||||
await startSimulator();
|
||||
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// create meter
|
||||
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
|
||||
await meterModal.getByLabel("Title").fill("North Roof");
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("shelly-1pm");
|
||||
await meterModal.getByLabel("IP address or hostname").fill(simulatorHost());
|
||||
await meterModal.getByRole("button", { name: "Validate & save" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toBeVisible();
|
||||
await expect(page.getByTestId("pv")).toContainText("North Roof");
|
||||
|
||||
// break meter
|
||||
await stopSimulator();
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
|
||||
// remove meter
|
||||
await expect(page.getByTestId("fatal-error")).toBeVisible();
|
||||
await expect(page.getByTestId("pv")).toBeVisible();
|
||||
await page.getByTestId("pv").getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByRole("button", { name: "Delete" }).click();
|
||||
await expectModalHidden(meterModal);
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
|
||||
// restart and check again
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId("pv")).toHaveCount(0);
|
||||
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -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("")
|
||||
);
|
||||
});
|
||||
});
|
||||
106
tests/config-tariffs.spec.ts
Normal file
106
tests/config-tariffs.spec.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import {
|
||||
enableExperimental,
|
||||
expectModalHidden,
|
||||
expectModalVisible,
|
||||
editorClear,
|
||||
editorPaste,
|
||||
} from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
const CONFIG_WITH_TARIFFS = "config-with-tariffs.evcc.yaml";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
async function goToConfig(page: Page) {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
}
|
||||
|
||||
test.describe("tariffs", async () => {
|
||||
test("tariffs not configured", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
|
||||
await expect(page.getByTestId("tariffs")).not.toBeVisible();
|
||||
await expect(page.getByTestId("add-tariffs")).toBeVisible();
|
||||
});
|
||||
|
||||
test("tariffs via ui", async ({ page }) => {
|
||||
await start(CONFIG_GRID_ONLY);
|
||||
await goToConfig(page);
|
||||
|
||||
await page.getByTestId("add-tariffs").click();
|
||||
const modal = await page.getByTestId("tariffs-modal");
|
||||
await expectModalVisible(modal);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// default content
|
||||
const editor = modal.getByTestId("yaml-editor");
|
||||
await expect(editor).toContainText("#currency: EUR");
|
||||
|
||||
// clear and enter invalid yaml
|
||||
await editorClear(editor);
|
||||
await editorPaste(editor, page, "foo: bar");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(modal.getByTestId("error")).toContainText("invalid keys: foo");
|
||||
|
||||
// clear and enter valid yaml
|
||||
await editorClear(editor);
|
||||
await editorPaste(
|
||||
editor,
|
||||
page,
|
||||
`currency: CHF
|
||||
grid:
|
||||
type: fixed
|
||||
price: 0.123`
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(modal.getByTestId("error")).not.toBeVisible();
|
||||
|
||||
// modal closes
|
||||
await expectModalHidden(modal);
|
||||
|
||||
// restart button appears
|
||||
const restartButton = await page
|
||||
.getByTestId("bottom-banner")
|
||||
.getByRole("button", { name: "Restart" });
|
||||
await expect(restartButton).toBeVisible();
|
||||
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
|
||||
// restart done
|
||||
await expect(restartButton).not.toBeVisible();
|
||||
|
||||
await expect(page.getByTestId("tariffs")).toContainText(
|
||||
["Currency", "CHF", "Grid price", "12.3 rp"].join("")
|
||||
);
|
||||
});
|
||||
|
||||
test("tariffs from evcc.yaml", async ({ page }) => {
|
||||
await start(CONFIG_WITH_TARIFFS);
|
||||
await goToConfig(page);
|
||||
|
||||
await expect(page.getByTestId("tariffs")).toBeVisible();
|
||||
await expect(page.getByTestId("tariffs")).toContainText(
|
||||
[
|
||||
"Tariffs",
|
||||
"Currency",
|
||||
"SEK",
|
||||
"Grid price",
|
||||
"30.0 öre",
|
||||
"Feed-in price",
|
||||
"-10.0 öre",
|
||||
"Grid CO₂",
|
||||
"300 g",
|
||||
].join("")
|
||||
);
|
||||
});
|
||||
});
|
||||
136
tests/evcc.js
136
tests/evcc.js
|
|
@ -1,136 +0,0 @@
|
|||
import fs from "fs";
|
||||
import waitOn from "wait-on";
|
||||
import axios from "axios";
|
||||
import { spawn, execSync } from "child_process";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { Transform } from "stream";
|
||||
|
||||
const BINARY = "./evcc";
|
||||
|
||||
function workerPort() {
|
||||
const index = process.env.TEST_WORKER_INDEX * 1;
|
||||
return 11000 + index;
|
||||
}
|
||||
|
||||
function logPrefix() {
|
||||
return `[worker:${process.env.TEST_WORKER_INDEX}]`;
|
||||
}
|
||||
|
||||
function createSteamLog() {
|
||||
return new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
const lines = chunk.toString().split("\n");
|
||||
lines.forEach((line) => {
|
||||
if (line.trim()) log(line);
|
||||
});
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function log(...args) {
|
||||
console.log(logPrefix(), ...args);
|
||||
}
|
||||
|
||||
export function baseUrl() {
|
||||
return `http://localhost:${workerPort()}`;
|
||||
}
|
||||
|
||||
function dbPath() {
|
||||
const file = `evcc-${workerPort()}.db`;
|
||||
return path.join(os.tmpdir(), file);
|
||||
}
|
||||
|
||||
export async function start(config, sqlDumps, flags = "--disable-auth") {
|
||||
await _clean();
|
||||
if (sqlDumps) {
|
||||
await _restoreDatabase(sqlDumps);
|
||||
}
|
||||
return await _start(config, flags);
|
||||
}
|
||||
|
||||
export async function stop(instance) {
|
||||
await _stop(instance);
|
||||
await _clean();
|
||||
}
|
||||
|
||||
export async function restart(config, flags = "--disable-auth") {
|
||||
await _stop();
|
||||
await _start(config, flags);
|
||||
}
|
||||
|
||||
export async function cleanRestart(config, sqlDumps) {
|
||||
await _stop();
|
||||
await _clean();
|
||||
if (sqlDumps) {
|
||||
await _restoreDatabase(sqlDumps);
|
||||
}
|
||||
await _start(config);
|
||||
}
|
||||
|
||||
async function _restoreDatabase(sqlDumps) {
|
||||
const dumps = Array.isArray(sqlDumps) ? sqlDumps : [sqlDumps];
|
||||
for (const dump of dumps) {
|
||||
log("loading database", dbPath(), dump);
|
||||
execSync(`sqlite3 ${dbPath()} < tests/${dump}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function _start(config, flags = []) {
|
||||
const configFile = config.includes("/") ? config : `tests/${config}`;
|
||||
const port = workerPort();
|
||||
log(`wait until port ${port} is available`);
|
||||
// wait for port to be available
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
const additionalFlags = typeof flags === "string" ? [flags] : flags;
|
||||
additionalFlags.push("--log", "debug,httpd:trace");
|
||||
log("starting evcc", { config, port, additionalFlags });
|
||||
const instance = spawn(BINARY, ["--config", configFile, ...additionalFlags], {
|
||||
env: { EVCC_NETWORK_PORT: port.toString(), EVCC_DATABASE_DSN: dbPath() },
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const steamLog = createSteamLog();
|
||||
instance.stdout.pipe(steamLog);
|
||||
instance.stderr.pipe(steamLog);
|
||||
instance.on("exit", (code) => {
|
||||
log("evcc terminated", { code, port, config });
|
||||
steamLog.end();
|
||||
});
|
||||
await waitOn({ resources: [baseUrl()], log: true });
|
||||
return instance;
|
||||
}
|
||||
|
||||
async function _stop(instance) {
|
||||
const port = workerPort();
|
||||
if (instance) {
|
||||
log("shutting down evcc hard", { port });
|
||||
instance.kill("SIGKILL");
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
log("evcc is down", { port });
|
||||
return;
|
||||
}
|
||||
// check if auth is required
|
||||
const res = await axios.get(`${baseUrl()}/api/auth/status`);
|
||||
log("auth status", res.status, res.statusText, res.data);
|
||||
let cookie;
|
||||
// login required
|
||||
if (!res.data) {
|
||||
const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" });
|
||||
log("login", res.status, res.statusText);
|
||||
cookie = res.headers["set-cookie"];
|
||||
}
|
||||
log("shutting down evcc", { port });
|
||||
await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } });
|
||||
log(`wait until port ${port} is closed`);
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
log("evcc is down", { port });
|
||||
}
|
||||
|
||||
async function _clean() {
|
||||
const db = dbPath();
|
||||
if (fs.existsSync(db)) {
|
||||
log("delete database", db);
|
||||
fs.unlinkSync(db);
|
||||
}
|
||||
}
|
||||
140
tests/evcc.ts
Normal file
140
tests/evcc.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import fs from "fs";
|
||||
import waitOn from "wait-on";
|
||||
import axios from "axios";
|
||||
import { spawn, execSync, ChildProcess } from "child_process";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { Transform } from "stream";
|
||||
|
||||
const BINARY = "./evcc";
|
||||
|
||||
function workerPort() {
|
||||
const index = Number(process.env["TEST_WORKER_INDEX"] ?? 0);
|
||||
return 11000 + index;
|
||||
}
|
||||
|
||||
function logPrefix() {
|
||||
return `[worker:${process.env["TEST_WORKER_INDEX"]}]`;
|
||||
}
|
||||
|
||||
function createSteamLog() {
|
||||
return new Transform({
|
||||
transform(chunk: Buffer, _, callback) {
|
||||
const lines = chunk.toString().split("\n");
|
||||
lines.forEach((line: string) => {
|
||||
if (line.trim()) log(line);
|
||||
});
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function log(...args: any[]) {
|
||||
console.log(logPrefix(), ...args);
|
||||
}
|
||||
|
||||
export function baseUrl() {
|
||||
return `http://localhost:${workerPort()}`;
|
||||
}
|
||||
|
||||
function dbPath() {
|
||||
const file = `evcc-${workerPort()}.db`;
|
||||
return path.join(os.tmpdir(), file);
|
||||
}
|
||||
|
||||
export async function start(
|
||||
config: string,
|
||||
sqlDumps?: string | null,
|
||||
flags: string | string[] = "--disable-auth"
|
||||
) {
|
||||
await _clean();
|
||||
if (sqlDumps) {
|
||||
await _restoreDatabase(sqlDumps);
|
||||
}
|
||||
return await _start(config, flags);
|
||||
}
|
||||
|
||||
export async function stop(instance?: ChildProcess) {
|
||||
await _stop(instance);
|
||||
await _clean();
|
||||
}
|
||||
|
||||
export async function restart(config: string, flags = "--disable-auth") {
|
||||
await _stop();
|
||||
await _start(config, flags);
|
||||
}
|
||||
|
||||
export async function cleanRestart(config: string, sqlDumps: string) {
|
||||
await _stop();
|
||||
await _clean();
|
||||
if (sqlDumps) {
|
||||
await _restoreDatabase(sqlDumps);
|
||||
}
|
||||
await _start(config);
|
||||
}
|
||||
|
||||
async function _restoreDatabase(sqlDumps: string) {
|
||||
const dumps = Array.isArray(sqlDumps) ? sqlDumps : [sqlDumps];
|
||||
for (const dump of dumps) {
|
||||
log("loading database", dbPath(), dump);
|
||||
execSync(`sqlite3 ${dbPath()} < tests/${dump}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function _start(config: string, flags: string | string[] = []) {
|
||||
const configFile = config.includes("/") ? config : `tests/${config}`;
|
||||
const port = workerPort();
|
||||
log(`wait until port ${port} is available`);
|
||||
// wait for port to be available
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
const additionalFlags = typeof flags === "string" ? [flags] : flags;
|
||||
additionalFlags.push("--log", "debug,httpd:trace");
|
||||
log("starting evcc", { config, port, additionalFlags });
|
||||
const instance = spawn(BINARY, ["--config", configFile, ...additionalFlags], {
|
||||
env: { EVCC_NETWORK_PORT: port.toString(), EVCC_DATABASE_DSN: dbPath() },
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const steamLog = createSteamLog();
|
||||
instance.stdout.pipe(steamLog);
|
||||
instance.stderr.pipe(steamLog);
|
||||
instance.on("exit", (code) => {
|
||||
log("evcc terminated", { code, port, config });
|
||||
steamLog.end();
|
||||
});
|
||||
await waitOn({ resources: [baseUrl()], log: true });
|
||||
return instance;
|
||||
}
|
||||
|
||||
async function _stop(instance?: ChildProcess) {
|
||||
const port = workerPort();
|
||||
if (instance) {
|
||||
log("shutting down evcc hard", { port });
|
||||
instance.kill("SIGKILL");
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
log("evcc is down", { port });
|
||||
return;
|
||||
}
|
||||
// check if auth is required
|
||||
const res = await axios.get(`${baseUrl()}/api/auth/status`);
|
||||
log("auth status", res.status, res.statusText, res.data);
|
||||
let cookie;
|
||||
// login required
|
||||
if (!res.data) {
|
||||
const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" });
|
||||
log("login", res.status, res.statusText);
|
||||
cookie = res.headers["set-cookie"];
|
||||
}
|
||||
log("shutting down evcc", { port });
|
||||
await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } });
|
||||
log(`wait until port ${port} is closed`);
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
log("evcc is down", { port });
|
||||
}
|
||||
|
||||
async function _clean() {
|
||||
const db = dbPath();
|
||||
if (fs.existsSync(db)) {
|
||||
log("delete database", db);
|
||||
fs.unlinkSync(db);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,630 +0,0 @@
|
|||
import { test, expect, devices } from "@playwright/test";
|
||||
import { start, stop, baseUrl, restart } from "./evcc";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
const mobile = devices["iPhone 12 Mini"].viewport;
|
||||
|
||||
const CONFIG = "plan.evcc.yaml";
|
||||
const CONFIG_NO_TARIFF = "basics.evcc.yaml";
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await start(CONFIG);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
function getWeekday(offset = 1) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + offset);
|
||||
return date.toLocaleDateString("en-US", { weekday: "long" });
|
||||
}
|
||||
|
||||
async function setAndVerifyPlan(page, lp, { soc, energy }) {
|
||||
await lp.getByTestId("charging-plan-button").click();
|
||||
|
||||
if (soc) {
|
||||
await page.getByTestId("static-plan-soc").selectOption(soc);
|
||||
}
|
||||
if (energy) {
|
||||
// select "25 kWh (+50%)" by providing "25 kWh" as option text
|
||||
const optionText = await page
|
||||
.getByTestId("static-plan-energy")
|
||||
.locator("option", { hasText: energy })
|
||||
.textContent();
|
||||
await page.getByTestId("static-plan-energy").selectOption(optionText);
|
||||
}
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expect(lp.getByTestId("charging-plan")).toContainText(soc || energy);
|
||||
}
|
||||
|
||||
async function verifyRepeatingPlanAvailable(page, lp, expected) {
|
||||
await lp.getByTestId("charging-plan-button").click();
|
||||
if (expected) {
|
||||
await expect(page.getByTestId("repeating-plan-add")).toBeVisible();
|
||||
} else {
|
||||
await expect(page.getByTestId("repeating-plan-add")).not.toBeVisible();
|
||||
}
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
}
|
||||
|
||||
test.describe("basic functionality", async () => {
|
||||
test("vehicle with soc and capacity, set and restart", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await expect(lp1.getByTestId("plan-marker")).not.toBeVisible();
|
||||
await expect(lp1.getByText("Loadpoint", { exact: true })).toBeVisible();
|
||||
|
||||
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("90%");
|
||||
await lp1.getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
|
||||
await page.getByTestId("static-plan-day").selectOption({ index: 1 });
|
||||
await page.getByTestId("static-plan-time").fill("09:30");
|
||||
await page.getByTestId("static-plan-soc").selectOption("80%");
|
||||
await page.getByTestId("static-plan-precondition-lg-toggle").click();
|
||||
await page
|
||||
.getByTestId("static-plan-precondition-lg-select")
|
||||
.getByRole("combobox")
|
||||
.selectOption("1 hour");
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
|
||||
await expect(lp1.getByTestId("plan-marker")).toBeVisible();
|
||||
await expect(lp1.getByTestId("charging-plan").getByRole("button")).toHaveText(
|
||||
"tomorrow 09:3080%"
|
||||
);
|
||||
|
||||
await expect(lp1.getByTestId("vehicle-status-charger")).toHaveText("Connected.");
|
||||
await expect(lp1.getByTestId("vehicle-status-planstart")).toHaveText(/tomorrow .*/);
|
||||
await expect(lp1.getByTestId("plan-marker")).toBeVisible();
|
||||
await expect(lp1.getByTestId("charging-plan").getByRole("button")).toHaveText(
|
||||
"tomorrow 09:3080%"
|
||||
);
|
||||
await lp1.getByTestId("charging-plan").getByRole("button").click();
|
||||
await expect(page.getByTestId("static-plan-soc")).toHaveValue("80");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("vehicle variations", async () => {
|
||||
test.describe("guest vehicle", async () => {
|
||||
test("kWh based plan and limit, no repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
await expect(lp1.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
|
||||
|
||||
// kWh based limit
|
||||
await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
|
||||
|
||||
// kWh based plan
|
||||
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
|
||||
|
||||
// no repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp1, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("vehicle no soc no capacity", async () => {
|
||||
test("kWh based plan and limit, no repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle no SoC no Capacity");
|
||||
|
||||
// kWh based limit
|
||||
await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
|
||||
|
||||
// kWh based plan
|
||||
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
|
||||
|
||||
// no repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp1, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("vehicle no soc with capacity", async () => {
|
||||
test("kWh based plan and limit, no repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle no SoC with Capacity");
|
||||
|
||||
// kWh based limit
|
||||
await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh (+50%)");
|
||||
|
||||
// kWh based plan
|
||||
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
|
||||
|
||||
// no repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp1, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("vehicle with soc no capacity", async () => {
|
||||
test("kWh based plan and soc based limit, no repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC no Capacity");
|
||||
|
||||
// soc based limit
|
||||
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
|
||||
|
||||
// soc based plan
|
||||
await setAndVerifyPlan(page, lp1, { energy: "50 kWh" });
|
||||
|
||||
// no repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp1, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("vehicle with soc with capacity", async () => {
|
||||
test("soc based plan and limit, with repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
// soc based limit
|
||||
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
|
||||
|
||||
// soc based plan
|
||||
await setAndVerifyPlan(page, lp1, { soc: "60%" });
|
||||
|
||||
// repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp1, true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("loadpoint with soc, guest vehicle", async () => {
|
||||
test("kWh based plan and soc based limit, no repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp2 = await page.getByTestId("loadpoint").last();
|
||||
|
||||
// change vehicle
|
||||
await expect(lp2.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
|
||||
|
||||
// soc based limit
|
||||
await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
|
||||
|
||||
// soc based plan
|
||||
await setAndVerifyPlan(page, lp2, { energy: "50 kWh" });
|
||||
|
||||
// no repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp2, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("loadpoint with soc, vehicle with capacity", async () => {
|
||||
test("soc based plan and limit, with repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp2 = await page.getByTestId("loadpoint").last();
|
||||
|
||||
// change vehicle
|
||||
await lp2
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle no SoC with Capacity");
|
||||
|
||||
// soc based limit
|
||||
await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
|
||||
|
||||
// soc based plan
|
||||
await setAndVerifyPlan(page, lp2, { soc: "60%" });
|
||||
|
||||
// repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp2, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("preview", async () => {
|
||||
const cases = [
|
||||
{
|
||||
szenario: "kWh based plan",
|
||||
vehicle: "Vehicle no SoC with Capacity",
|
||||
goalId: "static-plan-energy",
|
||||
},
|
||||
{
|
||||
szenario: "soc based plan",
|
||||
vehicle: "Vehicle with SoC with Capacity",
|
||||
goalId: "static-plan-soc",
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach((c) => {
|
||||
test(c.szenario, async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1.getByTestId("change-vehicle").locator("select").selectOption(c.vehicle);
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
|
||||
// initial set -> preview plan
|
||||
await page.getByTestId("static-plan-day").selectOption({ index: 1 });
|
||||
await page.getByTestId("static-plan-time").fill("09:30");
|
||||
await page.getByTestId(c.goalId).selectOption("80");
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Preview plan");
|
||||
|
||||
// activate -> active plan
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
|
||||
|
||||
await page.getByTestId(c.goalId).selectOption("90");
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
|
||||
|
||||
// apply -> active plan
|
||||
await expect(page.getByTestId("static-plan-apply")).toBeVisible();
|
||||
await page.getByTestId("static-plan-apply").click();
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
|
||||
|
||||
await page.getByTestId("static-plan-time").fill("23:30");
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
|
||||
await expect(page.getByTestId("static-plan-apply")).toBeVisible();
|
||||
|
||||
// deactivate
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Preview plan");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("warnings", async () => {
|
||||
test("goal not reachable in time", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Massive Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
|
||||
// match this text but with fuzzy date "getByText('Goal will be reached 52:10 h')"
|
||||
await expect(page.getByTestId("plan-warnings")).toHaveText(/Goal will be reached .* later/);
|
||||
});
|
||||
test("time in the past", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
|
||||
await expect(page.getByTestId("plan-entry-warnings")).not.toBeVisible();
|
||||
|
||||
await page.getByTestId("static-plan-day").selectOption({ index: 0 });
|
||||
await page.getByTestId("static-plan-time").fill("00:01");
|
||||
|
||||
await expect(page.getByTestId("plan-entry-warnings")).toContainText(
|
||||
"Pick a time in the future, Marty."
|
||||
);
|
||||
await page.getByTestId("static-plan-time").fill("00:01");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("repeating", async () => {
|
||||
test("add and remove plans", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
const modal = await page.getByTestId("charging-plan-modal");
|
||||
|
||||
// one static plan, no number
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
|
||||
await expect(modal.getByTestId("plan-entry").first()).not.toContainText("#1");
|
||||
|
||||
// add plan
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(2);
|
||||
await expect(modal.getByTestId("plan-entry").first()).toContainText("#1");
|
||||
await expect(modal.getByTestId("plan-entry").last()).toContainText("#2");
|
||||
|
||||
// remove plan
|
||||
await modal.getByTestId("plan-entry").last().getByRole("button", { name: "Remove" }).click();
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
|
||||
await expect(modal.getByTestId("plan-entry").first()).not.toContainText("#1");
|
||||
});
|
||||
|
||||
test("preview static and repeating plan", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
const modal = await page.getByTestId("charging-plan-modal");
|
||||
|
||||
// one static plan
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
|
||||
|
||||
await modal.getByTestId("static-plan-day").selectOption({ index: 1 }); // tomorrow
|
||||
await modal.getByTestId("static-plan-time").fill("09:00");
|
||||
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Preview plan");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:00");
|
||||
|
||||
// add repeating plan
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
await modal.getByTestId("repeating-plan-weekdays").click();
|
||||
await modal.getByRole("checkbox", { name: "Select all" }).check();
|
||||
await modal.getByTestId("repeating-plan-time").fill("11:11");
|
||||
|
||||
// switch between previews
|
||||
await modal
|
||||
.getByTestId("plan-preview-title")
|
||||
.getByRole("combobox")
|
||||
.selectOption("Preview plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("11:11");
|
||||
|
||||
await modal
|
||||
.getByTestId("plan-preview-title")
|
||||
.getByRole("combobox")
|
||||
.selectOption("Preview plan #1");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:00");
|
||||
|
||||
// activate #1
|
||||
await modal.getByTestId("static-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #1");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:00");
|
||||
|
||||
// activate #2
|
||||
await modal.getByTestId("static-plan-active").click();
|
||||
await modal.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("11:11");
|
||||
|
||||
// back to preview if no active plan
|
||||
await modal.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title").locator("option:checked")).toHaveText(
|
||||
"Preview plan #1"
|
||||
);
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:00");
|
||||
});
|
||||
|
||||
test("weekday selection", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
const modal = await page.getByTestId("charging-plan-modal");
|
||||
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
|
||||
// weekday select should have value "Mo-Fr"
|
||||
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
|
||||
"Mon – Fri"
|
||||
);
|
||||
|
||||
// select all weekdays
|
||||
await modal.getByTestId("repeating-plan-weekdays").click();
|
||||
await modal.getByRole("checkbox", { name: "Select all" }).click();
|
||||
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
|
||||
"Mon – Sun"
|
||||
);
|
||||
|
||||
// select none
|
||||
await modal.getByRole("checkbox", { name: "Select all" }).click();
|
||||
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText("–");
|
||||
|
||||
// select specific weekdays
|
||||
await modal.getByRole("checkbox", { name: "Thursday" }).check();
|
||||
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
|
||||
"Thu"
|
||||
);
|
||||
await modal.getByTestId("repeating-plan-weekdays").click(); // close
|
||||
|
||||
// activate
|
||||
await modal.getByTestId("repeating-plan-time").fill("02:22");
|
||||
await modal.getByTestId("repeating-plan-active").click();
|
||||
|
||||
// specific weekday and time
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("Thu 02:22");
|
||||
});
|
||||
|
||||
test("next plan", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const yesterday = getWeekday(-1);
|
||||
const tomorrow = getWeekday(1);
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
const modal = await page.getByTestId("charging-plan-modal");
|
||||
|
||||
// configure static plan for tomorrow
|
||||
const plan1 = modal.getByTestId("plan-entry").nth(0);
|
||||
await plan1.getByTestId("static-plan-day").selectOption({ index: 1 });
|
||||
await plan1.getByTestId("static-plan-time").fill("09:30");
|
||||
await plan1.getByTestId("static-plan-soc").selectOption("80%");
|
||||
await plan1.getByTestId("static-plan-active").click();
|
||||
|
||||
// add repeating plan for tomorrow
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
const plan2 = modal.getByTestId("plan-entry").nth(1);
|
||||
const days2 = plan2.getByTestId("repeating-plan-weekdays");
|
||||
await days2.click();
|
||||
await days2.getByRole("checkbox", { name: "Select all" }).click();
|
||||
await days2.getByRole("checkbox", { name: "Select all" }).click();
|
||||
await days2.getByRole("checkbox", { name: tomorrow }).check();
|
||||
await days2.click(); // close
|
||||
await plan2.getByTestId("repeating-plan-time").fill("09:20");
|
||||
await plan2.getByTestId("repeating-plan-active").click();
|
||||
|
||||
// add repeating plan for every day
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
const plan3 = modal.getByTestId("plan-entry").last();
|
||||
const days3 = plan3.getByTestId("repeating-plan-weekdays");
|
||||
await days3.click();
|
||||
await days3.getByRole("checkbox", { name: "Select all" }).check();
|
||||
await days3.click(); // close
|
||||
await plan3.getByTestId("repeating-plan-time").fill("09:10");
|
||||
await plan3.getByTestId("repeating-plan-active").click();
|
||||
|
||||
// check next plans
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #3");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:10");
|
||||
|
||||
// disable plan #3
|
||||
await plan3.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:20");
|
||||
|
||||
// change plan #2 to yesterday
|
||||
await days2.click();
|
||||
await days2.getByRole("checkbox", { name: tomorrow }).click(); // uncheck
|
||||
await days2.getByRole("checkbox", { name: yesterday }).click(); // check
|
||||
await days2.click(); // close
|
||||
// no changes yet
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:20");
|
||||
|
||||
// apply
|
||||
await plan2.getByTestId("repeating-plan-apply").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #1");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:30");
|
||||
|
||||
// set lower targets than vehicle soc (50%)
|
||||
await plan1.getByTestId("static-plan-soc").selectOption("40%");
|
||||
await plan1.getByTestId("static-plan-apply").click();
|
||||
await plan2.getByTestId("repeating-plan-soc").selectOption("40%");
|
||||
await plan2.getByTestId("repeating-plan-apply").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Goal already reached");
|
||||
});
|
||||
|
||||
test("repeating plan persistence", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const tomorrow = getWeekday(1);
|
||||
|
||||
let lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
let modal = await page.getByTestId("charging-plan-modal");
|
||||
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
const plan = modal.getByTestId("plan-entry").nth(1);
|
||||
await plan.getByTestId("repeating-plan-weekdays").click();
|
||||
await plan.getByRole("checkbox", { name: "Select all" }).click(); // check all
|
||||
await plan.getByRole("checkbox", { name: "Select all" }).click(); // uncheck all
|
||||
await plan.getByRole("checkbox", { name: tomorrow }).check();
|
||||
await plan.getByTestId("repeating-plan-time").fill("09:20");
|
||||
await plan.getByTestId("repeating-plan-precondition-lg-toggle").click();
|
||||
await plan
|
||||
.getByTestId("repeating-plan-precondition-lg-select")
|
||||
.getByRole("combobox")
|
||||
.selectOption("2 hours");
|
||||
await plan.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:20");
|
||||
|
||||
await restart(CONFIG);
|
||||
await page.goto("/");
|
||||
|
||||
lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "tomorrow 09:20" }).click();
|
||||
modal = await page.getByTestId("charging-plan-modal");
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(2);
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:20");
|
||||
await expect(modal.getByTestId("repeating-plan-precondition-lg-toggle")).toBeChecked();
|
||||
await expect(
|
||||
modal.getByTestId("repeating-plan-precondition-lg-select").locator("option:checked")
|
||||
).toHaveText("2 hours");
|
||||
});
|
||||
});
|
||||
|
||||
// add test for precondition, start with basic.evcc.yaml and verify that precondition toggle element is not visible. make dedicated describe block
|
||||
test.describe("precondition", async () => {
|
||||
test("only if dynamic tariff exists", async ({ page }) => {
|
||||
await restart(CONFIG_NO_TARIFF);
|
||||
await page.goto("/");
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
await expect(page.getByTestId("static-plan-active")).toBeVisible();
|
||||
await expect(page.getByTestId("static-plan-precondition-lg-toggle")).not.toBeVisible();
|
||||
await expect(page.getByTestId("static-plan-precondition-lg-select")).not.toBeVisible();
|
||||
|
||||
// verify small viewport
|
||||
await page.setViewportSize(mobile);
|
||||
await expect(page.getByTestId("static-plan-precondition-select")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
646
tests/plan.spec.ts
Normal file
646
tests/plan.spec.ts
Normal file
|
|
@ -0,0 +1,646 @@
|
|||
import { test, expect, devices, type Page, type Locator } from "@playwright/test";
|
||||
import { start, stop, baseUrl, restart } from "./evcc";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
const mobile = devices["iPhone 12 Mini"].viewport;
|
||||
|
||||
const CONFIG = "plan.evcc.yaml";
|
||||
const CONFIG_NO_TARIFF = "basics.evcc.yaml";
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await start(CONFIG);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
function getWeekday(offset = 1) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + offset);
|
||||
return date.toLocaleDateString("en-US", { weekday: "long" });
|
||||
}
|
||||
|
||||
async function setAndVerifyPlan(
|
||||
page: Page,
|
||||
lp: Locator,
|
||||
{ soc, energy }: { soc?: string; energy?: string }
|
||||
) {
|
||||
await lp.getByTestId("charging-plan-button").click();
|
||||
|
||||
if (soc) {
|
||||
await page.getByTestId("static-plan-soc").selectOption(soc);
|
||||
}
|
||||
if (energy) {
|
||||
// select "25 kWh (+50%)" by providing "25 kWh" as option text
|
||||
const optionText = await page
|
||||
.getByTestId("static-plan-energy")
|
||||
.locator("option", { hasText: energy })
|
||||
.textContent();
|
||||
await page.getByTestId("static-plan-energy").selectOption(optionText);
|
||||
}
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expect(lp.getByTestId("charging-plan")).toContainText(soc || energy!);
|
||||
}
|
||||
|
||||
async function verifyRepeatingPlanAvailable(page: Page, lp: Locator, expected: boolean) {
|
||||
await lp.getByTestId("charging-plan-button").click();
|
||||
if (expected) {
|
||||
await expect(page.getByTestId("repeating-plan-add")).toBeVisible();
|
||||
} else {
|
||||
await expect(page.getByTestId("repeating-plan-add")).not.toBeVisible();
|
||||
}
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
}
|
||||
|
||||
test.describe("basic functionality", async () => {
|
||||
test("vehicle with soc and capacity, set and restart", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await expect(lp1.getByTestId("plan-marker")).not.toBeVisible();
|
||||
await expect(lp1.getByText("Loadpoint", { exact: true })).toBeVisible();
|
||||
|
||||
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("90%");
|
||||
await lp1.getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
|
||||
await page.getByTestId("static-plan-day").selectOption({ index: 1 });
|
||||
await page.getByTestId("static-plan-time").fill("09:30");
|
||||
await page.getByTestId("static-plan-soc").selectOption("80%");
|
||||
await page.getByTestId("static-plan-precondition-lg-toggle").click();
|
||||
await page
|
||||
.getByTestId("static-plan-precondition-lg-select")
|
||||
.getByRole("combobox")
|
||||
.selectOption("1 hour");
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
|
||||
await expect(lp1.getByTestId("plan-marker")).toBeVisible();
|
||||
await expect(lp1.getByTestId("charging-plan").getByRole("button")).toHaveText(
|
||||
"tomorrow 09:3080%"
|
||||
);
|
||||
|
||||
await expect(lp1.getByTestId("vehicle-status-charger")).toHaveText("Connected.");
|
||||
await expect(lp1.getByTestId("vehicle-status-planstart")).toHaveText(/tomorrow .*/);
|
||||
await expect(lp1.getByTestId("plan-marker")).toBeVisible();
|
||||
await expect(lp1.getByTestId("charging-plan").getByRole("button")).toHaveText(
|
||||
"tomorrow 09:3080%"
|
||||
);
|
||||
await lp1.getByTestId("charging-plan").getByRole("button").click();
|
||||
await expect(page.getByTestId("static-plan-soc")).toHaveValue("80");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("vehicle variations", async () => {
|
||||
test.describe("guest vehicle", async () => {
|
||||
test("kWh based plan and limit, no repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
await expect(lp1.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
|
||||
|
||||
// kWh based limit
|
||||
await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
|
||||
|
||||
// kWh based plan
|
||||
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
|
||||
|
||||
// no repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp1, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("vehicle no soc no capacity", async () => {
|
||||
test("kWh based plan and limit, no repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle no SoC no Capacity");
|
||||
|
||||
// kWh based limit
|
||||
await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
|
||||
|
||||
// kWh based plan
|
||||
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
|
||||
|
||||
// no repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp1, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("vehicle no soc with capacity", async () => {
|
||||
test("kWh based plan and limit, no repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle no SoC with Capacity");
|
||||
|
||||
// kWh based limit
|
||||
await lp1
|
||||
.getByTestId("limit-energy")
|
||||
.getByRole("combobox")
|
||||
.selectOption("50 kWh (+50%)");
|
||||
|
||||
// kWh based plan
|
||||
await setAndVerifyPlan(page, lp1, { energy: "25 kWh" });
|
||||
|
||||
// no repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp1, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("vehicle with soc no capacity", async () => {
|
||||
test("kWh based plan and soc based limit, no repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC no Capacity");
|
||||
|
||||
// soc based limit
|
||||
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
|
||||
|
||||
// soc based plan
|
||||
await setAndVerifyPlan(page, lp1, { energy: "50 kWh" });
|
||||
|
||||
// no repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp1, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("vehicle with soc with capacity", async () => {
|
||||
test("soc based plan and limit, with repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
// soc based limit
|
||||
await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
|
||||
|
||||
// soc based plan
|
||||
await setAndVerifyPlan(page, lp1, { soc: "60%" });
|
||||
|
||||
// repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp1, true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("loadpoint with soc, guest vehicle", async () => {
|
||||
test("kWh based plan and soc based limit, no repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp2 = await page.getByTestId("loadpoint").last();
|
||||
|
||||
// change vehicle
|
||||
await expect(lp2.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
|
||||
|
||||
// soc based limit
|
||||
await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
|
||||
|
||||
// soc based plan
|
||||
await setAndVerifyPlan(page, lp2, { energy: "50 kWh" });
|
||||
|
||||
// no repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp2, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("loadpoint with soc, vehicle with capacity", async () => {
|
||||
test("soc based plan and limit, with repeats", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp2 = await page.getByTestId("loadpoint").last();
|
||||
|
||||
// change vehicle
|
||||
await lp2
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle no SoC with Capacity");
|
||||
|
||||
// soc based limit
|
||||
await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
|
||||
|
||||
// soc based plan
|
||||
await setAndVerifyPlan(page, lp2, { soc: "60%" });
|
||||
|
||||
// repeating plans option
|
||||
await verifyRepeatingPlanAvailable(page, lp2, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("preview", async () => {
|
||||
const cases = [
|
||||
{
|
||||
szenario: "kWh based plan",
|
||||
vehicle: "Vehicle no SoC with Capacity",
|
||||
goalId: "static-plan-energy",
|
||||
},
|
||||
{
|
||||
szenario: "soc based plan",
|
||||
vehicle: "Vehicle with SoC with Capacity",
|
||||
goalId: "static-plan-soc",
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach((c) => {
|
||||
test(c.szenario, async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1.getByTestId("change-vehicle").locator("select").selectOption(c.vehicle);
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
|
||||
// initial set -> preview plan
|
||||
await page.getByTestId("static-plan-day").selectOption({ index: 1 });
|
||||
await page.getByTestId("static-plan-time").fill("09:30");
|
||||
await page.getByTestId(c.goalId).selectOption("80");
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Preview plan");
|
||||
|
||||
// activate -> active plan
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
|
||||
|
||||
await page.getByTestId(c.goalId).selectOption("90");
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
|
||||
|
||||
// apply -> active plan
|
||||
await expect(page.getByTestId("static-plan-apply")).toBeVisible();
|
||||
await page.getByTestId("static-plan-apply").click();
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
|
||||
|
||||
await page.getByTestId("static-plan-time").fill("23:30");
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Active plan");
|
||||
await expect(page.getByTestId("static-plan-apply")).toBeVisible();
|
||||
|
||||
// deactivate
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
await expect(page.getByTestId("plan-preview-title")).toHaveText("Preview plan");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("warnings", async () => {
|
||||
test("goal not reachable in time", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Massive Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
|
||||
await page.getByTestId("static-plan-active").click();
|
||||
|
||||
// match this text but with fuzzy date "getByText('Goal will be reached 52:10 h')"
|
||||
await expect(page.getByTestId("plan-warnings")).toHaveText(/Goal will be reached .* later/);
|
||||
});
|
||||
test("time in the past", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
|
||||
// change vehicle
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
|
||||
await expect(page.getByTestId("plan-entry-warnings")).not.toBeVisible();
|
||||
|
||||
await page.getByTestId("static-plan-day").selectOption({ index: 0 });
|
||||
await page.getByTestId("static-plan-time").fill("00:01");
|
||||
|
||||
await expect(page.getByTestId("plan-entry-warnings")).toContainText(
|
||||
"Pick a time in the future, Marty."
|
||||
);
|
||||
await page.getByTestId("static-plan-time").fill("00:01");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("repeating", async () => {
|
||||
test("add and remove plans", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
const modal = await page.getByTestId("charging-plan-modal");
|
||||
|
||||
// one static plan, no number
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
|
||||
await expect(modal.getByTestId("plan-entry").first()).not.toContainText("#1");
|
||||
|
||||
// add plan
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(2);
|
||||
await expect(modal.getByTestId("plan-entry").first()).toContainText("#1");
|
||||
await expect(modal.getByTestId("plan-entry").last()).toContainText("#2");
|
||||
|
||||
// remove plan
|
||||
await modal
|
||||
.getByTestId("plan-entry")
|
||||
.last()
|
||||
.getByRole("button", { name: "Remove" })
|
||||
.click();
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
|
||||
await expect(modal.getByTestId("plan-entry").first()).not.toContainText("#1");
|
||||
});
|
||||
|
||||
test("preview static and repeating plan", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
const modal = await page.getByTestId("charging-plan-modal");
|
||||
|
||||
// one static plan
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(1);
|
||||
|
||||
await modal.getByTestId("static-plan-day").selectOption({ index: 1 }); // tomorrow
|
||||
await modal.getByTestId("static-plan-time").fill("09:00");
|
||||
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Preview plan");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:00");
|
||||
|
||||
// add repeating plan
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
await modal.getByTestId("repeating-plan-weekdays").click();
|
||||
await modal.getByRole("checkbox", { name: "Select all" }).check();
|
||||
await modal.getByTestId("repeating-plan-time").fill("11:11");
|
||||
|
||||
// switch between previews
|
||||
await modal
|
||||
.getByTestId("plan-preview-title")
|
||||
.getByRole("combobox")
|
||||
.selectOption("Preview plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("11:11");
|
||||
|
||||
await modal
|
||||
.getByTestId("plan-preview-title")
|
||||
.getByRole("combobox")
|
||||
.selectOption("Preview plan #1");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:00");
|
||||
|
||||
// activate #1
|
||||
await modal.getByTestId("static-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #1");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:00");
|
||||
|
||||
// activate #2
|
||||
await modal.getByTestId("static-plan-active").click();
|
||||
await modal.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("11:11");
|
||||
|
||||
// back to preview if no active plan
|
||||
await modal.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title").locator("option:checked")).toHaveText(
|
||||
"Preview plan #1"
|
||||
);
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:00");
|
||||
});
|
||||
|
||||
test("weekday selection", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
const modal = await page.getByTestId("charging-plan-modal");
|
||||
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
|
||||
// weekday select should have value "Mo-Fr"
|
||||
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
|
||||
"Mon – Fri"
|
||||
);
|
||||
|
||||
// select all weekdays
|
||||
await modal.getByTestId("repeating-plan-weekdays").click();
|
||||
await modal.getByRole("checkbox", { name: "Select all" }).click();
|
||||
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
|
||||
"Mon – Sun"
|
||||
);
|
||||
|
||||
// select none
|
||||
await modal.getByRole("checkbox", { name: "Select all" }).click();
|
||||
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
|
||||
"–"
|
||||
);
|
||||
|
||||
// select specific weekdays
|
||||
await modal.getByRole("checkbox", { name: "Thursday" }).check();
|
||||
await expect(modal.getByTestId("repeating-plan-weekdays").getByRole("button")).toHaveText(
|
||||
"Thu"
|
||||
);
|
||||
await modal.getByTestId("repeating-plan-weekdays").click(); // close
|
||||
|
||||
// activate
|
||||
await modal.getByTestId("repeating-plan-time").fill("02:22");
|
||||
await modal.getByTestId("repeating-plan-active").click();
|
||||
|
||||
// specific weekday and time
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("Thu 02:22");
|
||||
});
|
||||
|
||||
test("next plan", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const yesterday = getWeekday(-1);
|
||||
const tomorrow = getWeekday(1);
|
||||
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
const modal = await page.getByTestId("charging-plan-modal");
|
||||
|
||||
// configure static plan for tomorrow
|
||||
const plan1 = modal.getByTestId("plan-entry").nth(0);
|
||||
await plan1.getByTestId("static-plan-day").selectOption({ index: 1 });
|
||||
await plan1.getByTestId("static-plan-time").fill("09:30");
|
||||
await plan1.getByTestId("static-plan-soc").selectOption("80%");
|
||||
await plan1.getByTestId("static-plan-active").click();
|
||||
|
||||
// add repeating plan for tomorrow
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
const plan2 = modal.getByTestId("plan-entry").nth(1);
|
||||
const days2 = plan2.getByTestId("repeating-plan-weekdays");
|
||||
await days2.click();
|
||||
await days2.getByRole("checkbox", { name: "Select all" }).click();
|
||||
await days2.getByRole("checkbox", { name: "Select all" }).click();
|
||||
await days2.getByRole("checkbox", { name: tomorrow }).check();
|
||||
await days2.click(); // close
|
||||
await plan2.getByTestId("repeating-plan-time").fill("09:20");
|
||||
await plan2.getByTestId("repeating-plan-active").click();
|
||||
|
||||
// add repeating plan for every day
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
const plan3 = modal.getByTestId("plan-entry").last();
|
||||
const days3 = plan3.getByTestId("repeating-plan-weekdays");
|
||||
await days3.click();
|
||||
await days3.getByRole("checkbox", { name: "Select all" }).check();
|
||||
await days3.click(); // close
|
||||
await plan3.getByTestId("repeating-plan-time").fill("09:10");
|
||||
await plan3.getByTestId("repeating-plan-active").click();
|
||||
|
||||
// check next plans
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #3");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:10");
|
||||
|
||||
// disable plan #3
|
||||
await plan3.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:20");
|
||||
|
||||
// change plan #2 to yesterday
|
||||
await days2.click();
|
||||
await days2.getByRole("checkbox", { name: tomorrow }).click(); // uncheck
|
||||
await days2.getByRole("checkbox", { name: yesterday }).click(); // check
|
||||
await days2.click(); // close
|
||||
// no changes yet
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:20");
|
||||
|
||||
// apply
|
||||
await plan2.getByTestId("repeating-plan-apply").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #1");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("9:30");
|
||||
|
||||
// set lower targets than vehicle soc (50%)
|
||||
await plan1.getByTestId("static-plan-soc").selectOption("40%");
|
||||
await plan1.getByTestId("static-plan-apply").click();
|
||||
await plan2.getByTestId("repeating-plan-soc").selectOption("40%");
|
||||
await plan2.getByTestId("repeating-plan-apply").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Goal already reached");
|
||||
});
|
||||
|
||||
test("repeating plan persistence", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const tomorrow = getWeekday(1);
|
||||
|
||||
let lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
let modal = await page.getByTestId("charging-plan-modal");
|
||||
|
||||
await modal.getByRole("button", { name: "Add repeating plan" }).click();
|
||||
const plan = modal.getByTestId("plan-entry").nth(1);
|
||||
await plan.getByTestId("repeating-plan-weekdays").click();
|
||||
await plan.getByRole("checkbox", { name: "Select all" }).click(); // check all
|
||||
await plan.getByRole("checkbox", { name: "Select all" }).click(); // uncheck all
|
||||
await plan.getByRole("checkbox", { name: tomorrow }).check();
|
||||
await plan.getByTestId("repeating-plan-time").fill("09:20");
|
||||
await plan.getByTestId("repeating-plan-precondition-lg-toggle").click();
|
||||
await plan
|
||||
.getByTestId("repeating-plan-precondition-lg-select")
|
||||
.getByRole("combobox")
|
||||
.selectOption("2 hours");
|
||||
await plan.getByTestId("repeating-plan-active").click();
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:20");
|
||||
|
||||
await restart(CONFIG);
|
||||
await page.goto("/");
|
||||
|
||||
lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1
|
||||
.getByTestId("change-vehicle")
|
||||
.locator("select")
|
||||
.selectOption("Vehicle with SoC with Capacity");
|
||||
|
||||
await lp1
|
||||
.getByTestId("charging-plan")
|
||||
.getByRole("button", { name: "tomorrow 09:20" })
|
||||
.click();
|
||||
modal = await page.getByTestId("charging-plan-modal");
|
||||
await expect(modal.getByTestId("plan-entry")).toHaveCount(2);
|
||||
await expect(modal.getByTestId("plan-preview-title")).toHaveText("Next plan #2");
|
||||
await expect(modal.getByTestId("target-text")).toContainText("09:20");
|
||||
await expect(modal.getByTestId("repeating-plan-precondition-lg-toggle")).toBeChecked();
|
||||
await expect(
|
||||
modal.getByTestId("repeating-plan-precondition-lg-select").locator("option:checked")
|
||||
).toHaveText("2 hours");
|
||||
});
|
||||
});
|
||||
|
||||
// add test for precondition, start with basic.evcc.yaml and verify that precondition toggle element is not visible. make dedicated describe block
|
||||
test.describe("precondition", async () => {
|
||||
test("only if dynamic tariff exists", async ({ page }) => {
|
||||
await restart(CONFIG_NO_TARIFF);
|
||||
await page.goto("/");
|
||||
const lp1 = await page.getByTestId("loadpoint").first();
|
||||
await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
await expect(page.getByTestId("static-plan-active")).toBeVisible();
|
||||
await expect(page.getByTestId("static-plan-precondition-lg-toggle")).not.toBeVisible();
|
||||
await expect(page.getByTestId("static-plan-precondition-lg-select")).not.toBeVisible();
|
||||
|
||||
// verify small viewport
|
||||
await page.setViewportSize(mobile);
|
||||
await expect(page.getByTestId("static-plan-precondition-select")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,332 +0,0 @@
|
|||
import { test, expect, devices } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
const mobile = devices["iPhone 12 Mini"].viewport;
|
||||
const desktop = devices["Desktop Chrome"].viewport;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("basics.evcc.yaml", "sessions.sql");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
async function selectLoadpointFilter(page, value) {
|
||||
await page
|
||||
.getByTestId("filter-loadpoint")
|
||||
.locator("visible=true")
|
||||
.getByRole("combobox")
|
||||
.selectOption(value);
|
||||
}
|
||||
|
||||
async function selectVehicleFilter(page, value) {
|
||||
await page
|
||||
.getByTestId("filter-vehicle")
|
||||
.locator("visible=true")
|
||||
.getByRole("combobox")
|
||||
.selectOption(value);
|
||||
}
|
||||
|
||||
test.describe("basics", async () => {
|
||||
test("navigation to sessions", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const topNavigationButton = await page.getByTestId("topnavigation-button");
|
||||
await topNavigationButton.click();
|
||||
await page
|
||||
.getByTestId("topnavigation-dropdown")
|
||||
.getByRole("link", { name: "Charging sessions" })
|
||||
.click();
|
||||
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
|
||||
});
|
||||
test("month without data", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(0);
|
||||
});
|
||||
test("month with data", async ({ page }) => {
|
||||
await page.setViewportSize(desktop);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("May");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
|
||||
await expect(page.getByRole("table")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-head")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(9);
|
||||
|
||||
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible("20.0");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible("67.3");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
|
||||
await expect(page.getByTestId("sessions-foot-price")).toBeVisible("5.50");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible("27.5");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-chargeDuration")).toContainText("Durationh:mm");
|
||||
await expect(page.getByTestId("sessions-foot-chargeDuration")).toBeVisible("1:30");
|
||||
|
||||
await page
|
||||
.getByTestId("sessions-head-chargeDuration")
|
||||
.getByRole("combobox")
|
||||
.selectOption("⌀ Power");
|
||||
await expect(page.getByTestId("sessions-head-avgPower")).toContainText("⌀ PowerkW");
|
||||
await expect(page.getByTestId("sessions-foot-avgPower")).toBeVisible("1:30");
|
||||
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("mobile basics", async () => {
|
||||
test("column select", async ({ page }) => {
|
||||
await page.setViewportSize(mobile);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(5);
|
||||
|
||||
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible("20.0");
|
||||
|
||||
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible("67.3");
|
||||
|
||||
await page.getByTestId("sessions-head-solar").getByRole("combobox").selectOption("Cost");
|
||||
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
|
||||
await expect(page.getByTestId("sessions-foot-price")).toBeVisible("5.50");
|
||||
|
||||
await page.getByTestId("sessions-head-price").getByRole("combobox").selectOption("⌀ Price");
|
||||
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible("27.5");
|
||||
});
|
||||
|
||||
test("keep selection when paging", async ({ page }) => {
|
||||
await page.setViewportSize(mobile);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
|
||||
await page.getByTestId("navigate-next-year-month").click();
|
||||
await page.getByTestId("navigate-prev-year-month").click();
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("paging", async () => {
|
||||
test("prev/next links", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
|
||||
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
|
||||
});
|
||||
test("next month", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await page.getByTestId("navigate-next-month").click();
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("June");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
|
||||
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
|
||||
});
|
||||
test("prev month", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await page.getByTestId("navigate-prev-month").click();
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("May");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
});
|
||||
});
|
||||
|
||||
for (const [name, viewport] of Object.entries({ desktop, mobile })) {
|
||||
test.describe(`filter on ${name}`, async () => {
|
||||
test("by vehicle", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
|
||||
|
||||
await selectVehicleFilter(page, "weißes Model 3 (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/weißes Model 3/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/weißes Model 3/);
|
||||
|
||||
await selectVehicleFilter(page, "all vehicles (4)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("by loadpoint", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(2)).toHaveText(/Carport/);
|
||||
|
||||
await selectLoadpointFilter(page, "Garage (1)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Garage/);
|
||||
|
||||
await selectLoadpointFilter(page, "all charging points (4)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("by vehicle and loadpoint", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await selectVehicleFilter(page, "weißes Model 3 (1)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveText(/weißes Model 3/);
|
||||
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
|
||||
});
|
||||
|
||||
test("by vehicle and loadpoint disabled options", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
const option = page
|
||||
.getByTestId("filter-loadpoint")
|
||||
.locator("visible=true")
|
||||
.locator("option[value=Garage]");
|
||||
await expect(option).toHaveAttribute("disabled", "");
|
||||
await expect(option).toHaveText("Garage (0)");
|
||||
});
|
||||
|
||||
test("keep filter when paging", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
if (name === "mobile") {
|
||||
await page.getByTestId("navigate-next-year-month").click();
|
||||
await page.getByTestId("navigate-prev-year-month").click();
|
||||
} else {
|
||||
await page.getByTestId("navigate-next-month").click();
|
||||
await page.getByTestId("navigate-prev-month").click();
|
||||
}
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("columns desktop", async () => {
|
||||
test("show vehicle column if multiple different exist", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("vehicle")).toBeVisible();
|
||||
});
|
||||
test("show loadpoint column if multiple different exist", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("loadpoint")).toBeVisible();
|
||||
});
|
||||
test("show co2 column it has values", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=3");
|
||||
await expect(page.getByTestId("sessions-head-co2")).toBeVisible();
|
||||
});
|
||||
test("hide co2 column if it doesnt have values", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-head-co2")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
const date = new Date();
|
||||
const YEAR = date.getFullYear();
|
||||
const MONTH = date.getMonth() + 1;
|
||||
const MONTH_YEAR = new Intl.DateTimeFormat("en", { month: "long", year: "numeric" }).format(date);
|
||||
|
||||
test.describe("csv export", async () => {
|
||||
test("total export", async ({ page }) => {
|
||||
await page.goto("/#/sessions?period=total");
|
||||
await expect(page.getByRole("link", { name: "Download total CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en"
|
||||
);
|
||||
});
|
||||
test("year export", async ({ page }) => {
|
||||
// fixed year
|
||||
await page.goto("/#/sessions?period=year&year=2023");
|
||||
await expect(page.getByRole("link", { name: "Download 2023 CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en&year=2023"
|
||||
);
|
||||
|
||||
// current year
|
||||
await page.goto(`/#/sessions?period=year`);
|
||||
await expect(page.getByRole("link", { name: `Download ${YEAR} CSV` })).toHaveAttribute(
|
||||
"href",
|
||||
`./api/sessions?format=csv&lang=en&year=${YEAR}`
|
||||
);
|
||||
});
|
||||
test("monthly export", async ({ page }) => {
|
||||
await page.goto("/#/sessions?&year=2023&month=5");
|
||||
await expect(page.getByRole("link", { name: "Download May 2023 CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en&year=2023&month=5"
|
||||
);
|
||||
|
||||
// current month
|
||||
await page.goto(`/#/sessions`);
|
||||
await expect(page.getByRole("link", { name: `Download ${MONTH_YEAR} CSV` })).toHaveAttribute(
|
||||
"href",
|
||||
`./api/sessions?format=csv&lang=en&year=${YEAR}&month=${MONTH}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("session details", async () => {
|
||||
test("show session details (session 5)", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await page.getByTestId("sessions-entry").nth(0).click();
|
||||
await expect(page.getByTestId("session-details")).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByTestId("session-details").getByRole("heading", { name: "Charging Session" })
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Garage");
|
||||
await expect(page.getByTestId("session-details-vehicle")).toContainText("weißes Model 3");
|
||||
await expect(page.getByTestId("session-details-date")).toContainText(
|
||||
["Thu, May 4, 22:00", "Fri, May 5, 06:00"].join("")
|
||||
);
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("5.0 kWh");
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
|
||||
await expect(page.getByTestId("session-details-solar")).toContainText("0.0% (0.0 kWh)");
|
||||
await expect(page.getByTestId("session-details-price")).toContainText("2.50 € 50.0 ct/kWh");
|
||||
await expect(page.getByTestId("session-details-co2")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-odometer")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-meter")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-delete")).toContainText("Delete");
|
||||
});
|
||||
|
||||
test("show session details with CO2 data (session 1)", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=3");
|
||||
await page.getByTestId("sessions-entry").nth(0).click();
|
||||
await expect(page.getByTestId("session-details")).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Carport");
|
||||
await expect(page.getByTestId("session-details-vehicle")).toContainText("blauer e-Golf");
|
||||
await expect(page.getByTestId("session-details-date")).toContainText(
|
||||
["Wed, Mar 1, 07:00", "Tue, May 2, 12:00"].join("")
|
||||
);
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("10.0 kWh");
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
|
||||
await expect(page.getByTestId("session-details-solar")).toContainText("100.0% (10.0 kWh)");
|
||||
await expect(page.getByTestId("session-details-price")).toContainText("2.00 € 20.0 ct/kWh");
|
||||
await expect(page.getByTestId("session-details-co2")).toContainText("300 g/kWh");
|
||||
});
|
||||
});
|
||||
343
tests/sessions.spec.ts
Normal file
343
tests/sessions.spec.ts
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
import { test, expect, devices, type Page } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
const mobile = devices["iPhone 12 Mini"].viewport;
|
||||
const desktop = devices["Desktop Chrome"].viewport;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start("basics.evcc.yaml", "sessions.sql");
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
async function selectLoadpointFilter(page: Page, value: string) {
|
||||
await page
|
||||
.getByTestId("filter-loadpoint")
|
||||
.locator("visible=true")
|
||||
.getByRole("combobox")
|
||||
.selectOption(value);
|
||||
}
|
||||
|
||||
async function selectVehicleFilter(page: Page, value: string) {
|
||||
await page
|
||||
.getByTestId("filter-vehicle")
|
||||
.locator("visible=true")
|
||||
.getByRole("combobox")
|
||||
.selectOption(value);
|
||||
}
|
||||
|
||||
test.describe("basics", async () => {
|
||||
test("navigation to sessions", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const topNavigationButton = await page.getByTestId("topnavigation-button");
|
||||
await topNavigationButton.click();
|
||||
await page
|
||||
.getByTestId("topnavigation-dropdown")
|
||||
.getByRole("link", { name: "Charging sessions" })
|
||||
.click();
|
||||
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
|
||||
});
|
||||
test("month without data", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(0);
|
||||
});
|
||||
test("month with data", async ({ page }) => {
|
||||
await page.setViewportSize(desktop);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("May");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
|
||||
await expect(page.getByRole("table")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-head")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(9);
|
||||
|
||||
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toHaveText("20.0");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toHaveText("67.3");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
|
||||
await expect(page.getByTestId("sessions-foot-price")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-price")).toHaveText("5.50");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toHaveText("27.5");
|
||||
|
||||
await expect(page.getByTestId("sessions-head-chargeDuration")).toContainText(
|
||||
"Durationh:mm"
|
||||
);
|
||||
await expect(page.getByTestId("sessions-foot-chargeDuration")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-chargeDuration")).toHaveText("1:30");
|
||||
|
||||
await page
|
||||
.getByTestId("sessions-head-chargeDuration")
|
||||
.getByRole("combobox")
|
||||
.selectOption("⌀ Power");
|
||||
await expect(page.getByTestId("sessions-head-avgPower")).toContainText("⌀ PowerkW");
|
||||
await expect(page.getByTestId("sessions-foot-avgPower")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-avgPower")).toHaveText("10.0");
|
||||
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("mobile basics", async () => {
|
||||
test("column select", async ({ page }) => {
|
||||
await page.setViewportSize(mobile);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(5);
|
||||
|
||||
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-energy")).toHaveText("20.0");
|
||||
|
||||
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-solar")).toHaveText("67.3");
|
||||
|
||||
await page.getByTestId("sessions-head-solar").getByRole("combobox").selectOption("Cost");
|
||||
await expect(page.getByTestId("sessions-head-price")).toContainText("Cost€");
|
||||
await expect(page.getByTestId("sessions-foot-price")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-price")).toHaveText("5.50");
|
||||
|
||||
await page.getByTestId("sessions-head-price").getByRole("combobox").selectOption("⌀ Price");
|
||||
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible();
|
||||
await expect(page.getByTestId("sessions-foot-avgPrice")).toHaveText("27.5");
|
||||
});
|
||||
|
||||
test("keep selection when paging", async ({ page }) => {
|
||||
await page.setViewportSize(mobile);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
|
||||
await page.getByTestId("navigate-next-year-month").click();
|
||||
await page.getByTestId("navigate-prev-year-month").click();
|
||||
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("paging", async () => {
|
||||
test("prev/next links", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
|
||||
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
|
||||
});
|
||||
test("next month", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await page.getByTestId("navigate-next-month").click();
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("June");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
await expect(page.getByTestId("navigate-next-month")).not.toBeDisabled();
|
||||
await expect(page.getByTestId("navigate-prev-month")).not.toBeDisabled();
|
||||
});
|
||||
test("prev month", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=6");
|
||||
await page.getByTestId("navigate-prev-month").click();
|
||||
await expect(page.getByTestId("navigate-month")).toHaveText("May");
|
||||
await expect(page.getByTestId("navigate-year")).toHaveText("2023");
|
||||
});
|
||||
});
|
||||
|
||||
for (const [name, viewport] of Object.entries({ desktop, mobile })) {
|
||||
test.describe(`filter on ${name}`, async () => {
|
||||
test("by vehicle", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
|
||||
|
||||
await selectVehicleFilter(page, "weißes Model 3 (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/weißes Model 3/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/weißes Model 3/);
|
||||
|
||||
await selectVehicleFilter(page, "all vehicles (4)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("by loadpoint", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(2)).toHaveText(/Carport/);
|
||||
|
||||
await selectLoadpointFilter(page, "Garage (1)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Garage/);
|
||||
|
||||
await selectLoadpointFilter(page, "all charging points (4)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("by vehicle and loadpoint", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await selectVehicleFilter(page, "weißes Model 3 (1)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(1);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveText(/weißes Model 3/);
|
||||
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(2);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(0)).toHaveText(/blauer e-Golf/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/Carport/);
|
||||
await expect(page.getByTestId("sessions-entry").nth(1)).toHaveText(/blauer e-Golf/);
|
||||
});
|
||||
|
||||
test("by vehicle and loadpoint disabled options", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await selectVehicleFilter(page, "blauer e-Golf (2)");
|
||||
const option = page
|
||||
.getByTestId("filter-loadpoint")
|
||||
.locator("visible=true")
|
||||
.locator("option[value=Garage]");
|
||||
await expect(option).toHaveAttribute("disabled", "");
|
||||
await expect(option).toHaveText("Garage (0)");
|
||||
});
|
||||
|
||||
test("keep filter when paging", async ({ page }) => {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
|
||||
await selectLoadpointFilter(page, "Carport (3)");
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
if (name === "mobile") {
|
||||
await page.getByTestId("navigate-next-year-month").click();
|
||||
await page.getByTestId("navigate-prev-year-month").click();
|
||||
} else {
|
||||
await page.getByTestId("navigate-next-month").click();
|
||||
await page.getByTestId("navigate-prev-month").click();
|
||||
}
|
||||
await expect(page.getByTestId("sessions-entry")).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("columns desktop", async () => {
|
||||
test("show vehicle column if multiple different exist", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("vehicle")).toBeVisible();
|
||||
});
|
||||
test("show loadpoint column if multiple different exist", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("loadpoint")).toBeVisible();
|
||||
});
|
||||
test("show co2 column it has values", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=3");
|
||||
await expect(page.getByTestId("sessions-head-co2")).toBeVisible();
|
||||
});
|
||||
test("hide co2 column if it doesnt have values", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await expect(page.getByTestId("sessions-head-co2")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
const date = new Date();
|
||||
const YEAR = date.getFullYear();
|
||||
const MONTH = date.getMonth() + 1;
|
||||
const MONTH_YEAR = new Intl.DateTimeFormat("en", { month: "long", year: "numeric" }).format(date);
|
||||
|
||||
test.describe("csv export", async () => {
|
||||
test("total export", async ({ page }) => {
|
||||
await page.goto("/#/sessions?period=total");
|
||||
await expect(page.getByRole("link", { name: "Download total CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en"
|
||||
);
|
||||
});
|
||||
test("year export", async ({ page }) => {
|
||||
// fixed year
|
||||
await page.goto("/#/sessions?period=year&year=2023");
|
||||
await expect(page.getByRole("link", { name: "Download 2023 CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en&year=2023"
|
||||
);
|
||||
|
||||
// current year
|
||||
await page.goto(`/#/sessions?period=year`);
|
||||
await expect(page.getByRole("link", { name: `Download ${YEAR} CSV` })).toHaveAttribute(
|
||||
"href",
|
||||
`./api/sessions?format=csv&lang=en&year=${YEAR}`
|
||||
);
|
||||
});
|
||||
test("monthly export", async ({ page }) => {
|
||||
await page.goto("/#/sessions?&year=2023&month=5");
|
||||
await expect(page.getByRole("link", { name: "Download May 2023 CSV" })).toHaveAttribute(
|
||||
"href",
|
||||
"./api/sessions?format=csv&lang=en&year=2023&month=5"
|
||||
);
|
||||
|
||||
// current month
|
||||
await page.goto(`/#/sessions`);
|
||||
await expect(
|
||||
page.getByRole("link", { name: `Download ${MONTH_YEAR} CSV` })
|
||||
).toHaveAttribute("href", `./api/sessions?format=csv&lang=en&year=${YEAR}&month=${MONTH}`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("session details", async () => {
|
||||
test("show session details (session 5)", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await page.getByTestId("sessions-entry").nth(0).click();
|
||||
await expect(page.getByTestId("session-details")).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByTestId("session-details").getByRole("heading", { name: "Charging Session" })
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Garage");
|
||||
await expect(page.getByTestId("session-details-vehicle")).toContainText("weißes Model 3");
|
||||
await expect(page.getByTestId("session-details-date")).toContainText(
|
||||
["Thu, May 4, 22:00", "Fri, May 5, 06:00"].join("")
|
||||
);
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("5.0 kWh");
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
|
||||
await expect(page.getByTestId("session-details-solar")).toContainText("0.0% (0.0 kWh)");
|
||||
await expect(page.getByTestId("session-details-price")).toContainText("2.50 € 50.0 ct/kWh");
|
||||
await expect(page.getByTestId("session-details-co2")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-odometer")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-meter")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-delete")).toContainText("Delete");
|
||||
});
|
||||
|
||||
test("show session details with CO2 data (session 1)", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=3");
|
||||
await page.getByTestId("sessions-entry").nth(0).click();
|
||||
await expect(page.getByTestId("session-details")).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Carport");
|
||||
await expect(page.getByTestId("session-details-vehicle")).toContainText("blauer e-Golf");
|
||||
await expect(page.getByTestId("session-details-date")).toContainText(
|
||||
["Wed, Mar 1, 07:00", "Tue, May 2, 12:00"].join("")
|
||||
);
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("10.0 kWh");
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
|
||||
await expect(page.getByTestId("session-details-solar")).toContainText("100.0% (10.0 kWh)");
|
||||
await expect(page.getByTestId("session-details-price")).toContainText("2.00 € 20.0 ct/kWh");
|
||||
await expect(page.getByTestId("session-details-co2")).toContainText("300 g/kWh");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import os from "os";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import waitOn from "wait-on";
|
||||
import axios from "axios";
|
||||
import { spawn } from "child_process";
|
||||
import { Transform } from "stream";
|
||||
|
||||
function workerPort() {
|
||||
const index = process.env.TEST_WORKER_INDEX * 1;
|
||||
return 12000 + index;
|
||||
}
|
||||
|
||||
function logPrefix() {
|
||||
return `[worker:${process.env.TEST_WORKER_INDEX}]`;
|
||||
}
|
||||
|
||||
function createSteamLog() {
|
||||
return new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
const lines = chunk.toString().split("\n");
|
||||
lines.forEach((line) => {
|
||||
if (line.trim()) log(line);
|
||||
});
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function log(...args) {
|
||||
console.log(logPrefix(), ...args);
|
||||
}
|
||||
|
||||
export function simulatorHost() {
|
||||
return `localhost:${workerPort()}`;
|
||||
}
|
||||
|
||||
export function simulatorUrl() {
|
||||
return `http://${simulatorHost()}`;
|
||||
}
|
||||
|
||||
export function simulatorConfig() {
|
||||
const input = "./tests/simulator.evcc.yaml";
|
||||
const content = fs.readFileSync(input, "utf8");
|
||||
const result = content.replace(/localhost:7072/g, simulatorHost());
|
||||
const resultName = "simulator.evcc.generated.yaml";
|
||||
const resultPath = path.join(os.tmpdir(), resultName);
|
||||
fs.writeFileSync(resultPath, result);
|
||||
return resultPath;
|
||||
}
|
||||
|
||||
export async function startSimulator() {
|
||||
const port = workerPort();
|
||||
log("starting simulator", { port });
|
||||
log(`wait until port ${port} is available`);
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
|
||||
const instance = spawn("npm", ["run", "simulator", "--", "--port", port]);
|
||||
|
||||
const steamLog = createSteamLog();
|
||||
instance.stdout.pipe(steamLog);
|
||||
instance.stderr.pipe(steamLog);
|
||||
instance.on("exit", (code) => {
|
||||
log("simulator terminated", { code, port });
|
||||
steamLog.end();
|
||||
});
|
||||
|
||||
await waitOn({ resources: [`${simulatorUrl()}/api/state`], log: true });
|
||||
}
|
||||
|
||||
export async function stopSimulator() {
|
||||
const port = workerPort();
|
||||
log("shutting down simulator", { port });
|
||||
await axios.post(`${simulatorUrl()}/api/shutdown`);
|
||||
log(`wait until port ${port} is closed`);
|
||||
await waitOn({ resources: [`tcp:localhost:${port}`], reverse: true });
|
||||
}
|
||||
77
tests/simulator.ts
Normal file
77
tests/simulator.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import os from "os";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import waitOn from "wait-on";
|
||||
import axios from "axios";
|
||||
import { spawn } from "child_process";
|
||||
import { Transform } from "stream";
|
||||
|
||||
function workerPort() {
|
||||
const index = parseInt(process.env["TEST_WORKER_INDEX"] ?? "-1");
|
||||
return 12000 + index;
|
||||
}
|
||||
|
||||
function logPrefix() {
|
||||
return `[worker:${process.env["TEST_WORKER_INDEX"]}]`;
|
||||
}
|
||||
|
||||
function createSteamLog() {
|
||||
return new Transform({
|
||||
transform(chunk: Buffer, _, callback) {
|
||||
const lines = chunk.toString().split("\n");
|
||||
lines.forEach((line) => {
|
||||
if (line.trim()) log(line);
|
||||
});
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function log(...args: any[]) {
|
||||
console.log(logPrefix(), ...args);
|
||||
}
|
||||
|
||||
export function simulatorHost() {
|
||||
return `localhost:${workerPort()}`;
|
||||
}
|
||||
|
||||
export function simulatorUrl() {
|
||||
return `http://${simulatorHost()}`;
|
||||
}
|
||||
|
||||
export function simulatorConfig() {
|
||||
const input = "./tests/simulator.evcc.yaml";
|
||||
const content = fs.readFileSync(input, "utf8");
|
||||
const result = content.replace(/localhost:7072/g, simulatorHost());
|
||||
const resultName = "simulator.evcc.generated.yaml";
|
||||
const resultPath = path.join(os.tmpdir(), resultName);
|
||||
fs.writeFileSync(resultPath, result);
|
||||
return resultPath;
|
||||
}
|
||||
|
||||
export async function startSimulator() {
|
||||
const port = workerPort();
|
||||
log("starting simulator", { port });
|
||||
log(`wait until port ${port} is available`);
|
||||
await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true });
|
||||
|
||||
const instance = spawn("npm", ["run", "simulator", "--", "--port", port.toString()]);
|
||||
|
||||
const steamLog = createSteamLog();
|
||||
instance.stdout.pipe(steamLog);
|
||||
instance.stderr.pipe(steamLog);
|
||||
instance.on("exit", (code) => {
|
||||
log("simulator terminated", { code, port });
|
||||
steamLog.end();
|
||||
});
|
||||
|
||||
await waitOn({ resources: [`${simulatorUrl()}/api/state`], log: true });
|
||||
}
|
||||
|
||||
export async function stopSimulator() {
|
||||
const port = workerPort();
|
||||
log("shutting down simulator", { port });
|
||||
await axios.post(`${simulatorUrl()}/api/shutdown`);
|
||||
log(`wait until port ${port} is closed`);
|
||||
await waitOn({ resources: [`tcp:localhost:${port}`], reverse: true });
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import bodyParser from "body-parser";
|
||||
|
||||
let state = {
|
||||
site: {
|
||||
grid: { power: 0 },
|
||||
pv: { power: 0 },
|
||||
battery: { power: 0, soc: 0 },
|
||||
},
|
||||
loadpoints: [{ power: 0, energy: 0, enabled: false, status: "A" }],
|
||||
vehicles: [{ soc: 0, range: 0 }],
|
||||
};
|
||||
|
||||
const loggingMiddleware = (req, res, next) => {
|
||||
console.log(`[simulator] ${req.method} ${req.originalUrl}`);
|
||||
next();
|
||||
};
|
||||
|
||||
const stateApiMiddleware = (req, res, next) => {
|
||||
if (req.method === "POST" && req.originalUrl === "/api/state") {
|
||||
console.log("[simulator] POST /api/state");
|
||||
state = req.body;
|
||||
res.end();
|
||||
} else if (req.method === "POST" && req.originalUrl === "/api/shutdown") {
|
||||
console.log("[simulator] POST /api/shutdown");
|
||||
res.end();
|
||||
process.exit();
|
||||
} else if (req.originalUrl === "/api/state") {
|
||||
res.end(JSON.stringify(state));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const openemsMiddleware = (req, res, next) => {
|
||||
const endpoints = {
|
||||
"/rest/channel/_sum/GridActivePower": { value: state.site.grid.power },
|
||||
"/rest/channel/_sum/ProductionActivePower": { value: state.site.pv.power },
|
||||
"/rest/channel/_sum/EssDischargePower": { value: state.site.battery.power },
|
||||
"/rest/channel/_sum/EssSoc": { value: state.site.battery.soc },
|
||||
};
|
||||
const endpoint = endpoints[req.originalUrl];
|
||||
if (req.method === "GET" && endpoint) {
|
||||
console.log("[simulator] GET", req.originalUrl);
|
||||
res.end(JSON.stringify(endpoint));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const teslaloggerMiddleware = (req, res, next) => {
|
||||
if (req.method === "GET" && req.originalUrl.startsWith("/currentjson/")) {
|
||||
const id = parseInt(req.originalUrl.split("/")[2]);
|
||||
const vehicle = state.vehicles[id - 1];
|
||||
if (!vehicle) {
|
||||
res.statusCode = 404;
|
||||
res.end(JSON.stringify({ error: "Vehicle not found" }));
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
battery_level: vehicle.soc,
|
||||
battery_range_km: vehicle.range,
|
||||
plugged_in: true,
|
||||
charging: false,
|
||||
odometer: 10000,
|
||||
is_preconditioning: false,
|
||||
charge_current_request: 10,
|
||||
};
|
||||
res.end(JSON.stringify(data));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const shellyMiddleware = (req, res, next) => {
|
||||
// simulate a shelly gen2 switch device api. implement power and energy
|
||||
if (req.originalUrl === "/shelly") {
|
||||
res.end(JSON.stringify({ gen: 2 }));
|
||||
} else if (req.originalUrl === "/rpc/Shelly.ListMethods") {
|
||||
res.end(JSON.stringify({ methods: ["Switch.GetStatus"] }));
|
||||
} else if (req.originalUrl === "/rpc/Switch.GetStatus?id=0") {
|
||||
res.end(
|
||||
JSON.stringify({ apower: state.site.pv.power, aenergy: { total: state.site.pv.energy } })
|
||||
);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
export default () => ({
|
||||
name: "api",
|
||||
enforce: "pre",
|
||||
configureServer(server) {
|
||||
console.log("[simulator] configured");
|
||||
return () => {
|
||||
server.middlewares.use(loggingMiddleware);
|
||||
server.middlewares.use(bodyParser.json());
|
||||
server.middlewares.use(stateApiMiddleware);
|
||||
server.middlewares.use(openemsMiddleware);
|
||||
server.middlewares.use(teslaloggerMiddleware);
|
||||
server.middlewares.use(shellyMiddleware);
|
||||
};
|
||||
},
|
||||
});
|
||||
129
tests/simulator/api.ts
Normal file
129
tests/simulator/api.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import bodyParser from "body-parser";
|
||||
import type { Connect, ViteDevServer } from "vite";
|
||||
import type { ServerResponse } from "http";
|
||||
|
||||
let state = {
|
||||
site: {
|
||||
grid: { power: 0 },
|
||||
pv: { power: 0, energy: 0 },
|
||||
battery: { power: 0, soc: 0 },
|
||||
},
|
||||
loadpoints: [{ power: 0, energy: 0, enabled: false, status: "A" }],
|
||||
vehicles: [{ soc: 0, range: 0 }],
|
||||
};
|
||||
|
||||
const loggingMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
_: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
console.log(`[simulator] ${req.method} ${req.url}`);
|
||||
next();
|
||||
};
|
||||
|
||||
const stateApiMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
if (req.method === "POST" && req.originalUrl === "/api/state") {
|
||||
console.log("[simulator] POST /api/state");
|
||||
// @ts-expect-error Property 'body' does not exist on type 'IncomingMessage'
|
||||
state = req.body;
|
||||
res.end();
|
||||
} else if (req.method === "POST" && req.originalUrl === "/api/shutdown") {
|
||||
console.log("[simulator] POST /api/shutdown");
|
||||
res.end();
|
||||
process.exit();
|
||||
} else if (req.originalUrl === "/api/state") {
|
||||
res.end(JSON.stringify(state));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const openemsMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
const endpoints = {
|
||||
"/rest/channel/_sum/GridActivePower": { value: state.site.grid.power },
|
||||
"/rest/channel/_sum/ProductionActivePower": { value: state.site.pv.power },
|
||||
"/rest/channel/_sum/EssDischargePower": { value: state.site.battery.power },
|
||||
"/rest/channel/_sum/EssSoc": { value: state.site.battery.soc },
|
||||
};
|
||||
const endpoint = endpoints[req.originalUrl as keyof typeof endpoints];
|
||||
if (req.method === "GET" && endpoint) {
|
||||
console.log("[simulator] GET", req.originalUrl);
|
||||
res.end(JSON.stringify(endpoint));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const teslaloggerMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
if (req.method === "GET" && req.originalUrl && req.originalUrl.startsWith("/currentjson/")) {
|
||||
const id = parseInt(req.originalUrl.split("/")[2]);
|
||||
const vehicle = state.vehicles[id - 1];
|
||||
if (!vehicle) {
|
||||
res.statusCode = 404;
|
||||
res.end(JSON.stringify({ error: "Vehicle not found" }));
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
battery_level: vehicle.soc,
|
||||
battery_range_km: vehicle.range,
|
||||
plugged_in: true,
|
||||
charging: false,
|
||||
odometer: 10000,
|
||||
is_preconditioning: false,
|
||||
charge_current_request: 10,
|
||||
};
|
||||
res.end(JSON.stringify(data));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const shellyMiddleware = (
|
||||
req: Connect.IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
) => {
|
||||
// simulate a shelly gen2 switch device api. implement power and energy
|
||||
if (req.originalUrl === "/shelly") {
|
||||
res.end(JSON.stringify({ gen: 2 }));
|
||||
} else if (req.originalUrl === "/rpc/Shelly.ListMethods") {
|
||||
res.end(JSON.stringify({ methods: ["Switch.GetStatus"] }));
|
||||
} else if (req.originalUrl === "/rpc/Switch.GetStatus?id=0") {
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
apower: state.site.pv.power,
|
||||
aenergy: { total: state.site.pv.energy },
|
||||
})
|
||||
);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
export default () => ({
|
||||
name: "api",
|
||||
enforce: "pre",
|
||||
configureServer(server: ViteDevServer) {
|
||||
console.log("[simulator] configured");
|
||||
return () => {
|
||||
server.middlewares.use(loggingMiddleware);
|
||||
server.middlewares.use(bodyParser.json());
|
||||
server.middlewares.use(stateApiMiddleware);
|
||||
server.middlewares.use(openemsMiddleware);
|
||||
server.middlewares.use(teslaloggerMiddleware);
|
||||
server.middlewares.use(shellyMiddleware);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -239,14 +239,28 @@
|
|||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import axios from "axios";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "Simulator",
|
||||
data() {
|
||||
return {
|
||||
state: null,
|
||||
state: null as {
|
||||
site: {
|
||||
grid: { power: number };
|
||||
pv: { power: number; energy: number };
|
||||
battery: { power: number; soc: number };
|
||||
};
|
||||
loadpoints: {
|
||||
power: number;
|
||||
energy: number;
|
||||
enabled: boolean;
|
||||
status: string;
|
||||
}[];
|
||||
vehicles: { soc: number; range: number }[];
|
||||
} | null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -262,24 +276,24 @@ export default {
|
|||
},
|
||||
addVehicle() {
|
||||
// push a duplacate of the last entry
|
||||
this.state.vehicles.push({
|
||||
this.state?.vehicles.push({
|
||||
...this.state.vehicles[this.state.vehicles.length - 1],
|
||||
});
|
||||
},
|
||||
removeVehicle(index) {
|
||||
this.state.vehicles.splice(index, 1);
|
||||
removeVehicle(index: number) {
|
||||
this.state?.vehicles.splice(index, 1);
|
||||
},
|
||||
addLoadpoint() {
|
||||
// push a duplacate of the last entry
|
||||
this.state.loadpoints.push({
|
||||
this.state?.loadpoints.push({
|
||||
...this.state.loadpoints[this.state.loadpoints.length - 1],
|
||||
});
|
||||
},
|
||||
removeLoadpoint(index) {
|
||||
this.state.loadpoints.splice(index, 1);
|
||||
removeLoadpoint(index: number) {
|
||||
this.state?.loadpoints.splice(index, 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import api from "./api";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), api()],
|
||||
server: { port: 7072, host: true },
|
||||
});
|
||||
12
tests/simulator/vite.config.ts
Normal file
12
tests/simulator/vite.config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import api from "./api";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
// @ts-expect-error the in api() declared 'enforce' property is not supported in TypeScript but in JavaScript
|
||||
api(),
|
||||
],
|
||||
server: { port: 7072, host: true },
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, Page, Locator } from "@playwright/test";
|
||||
import { expect, type Page, type Locator } from "@playwright/test";
|
||||
|
||||
export async function enableExperimental(page: Page): Promise<void> {
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "assets/js/**/*"],
|
||||
"include": ["env.d.ts", "assets/js/**/*", "tests/**/*"],
|
||||
"compilerOptions": {
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue