465 lines
16 KiB
TypeScript
465 lines
16 KiB
TypeScript
import { test, expect, type Page, type Locator } from "@playwright/test";
|
|
import { start, stop, restart, baseUrl } from "./evcc";
|
|
import { startSimulator, stopSimulator, simulatorHost } from "./simulator";
|
|
import {
|
|
expectModalVisible,
|
|
expectModalHidden,
|
|
addDemoCharger,
|
|
newLoadpoint,
|
|
finishLoadpoint,
|
|
openMoreMenu,
|
|
editorClear,
|
|
editorPaste,
|
|
} from "./utils";
|
|
|
|
test.use({ baseURL: baseUrl() });
|
|
test.describe.configure({ mode: "parallel" });
|
|
|
|
test.afterEach(async () => {
|
|
await stop();
|
|
});
|
|
|
|
async function expectNoFatal(page: Page) {
|
|
await expect(page.getByTestId("fatal-error")).not.toBeVisible();
|
|
}
|
|
|
|
function autoAcceptDialogs(page: Page) {
|
|
page.on("dialog", (dialog) => dialog.accept());
|
|
}
|
|
|
|
function disabledBadge(card: Locator) {
|
|
return card.getByRole("button", { name: "Enable" });
|
|
}
|
|
|
|
async function createLoadpoint(page: Page, title: string) {
|
|
await newLoadpoint(page, title);
|
|
await addDemoCharger(page);
|
|
// charger save instant-creates the loadpoint
|
|
await finishLoadpoint(page);
|
|
}
|
|
|
|
async function toggleLoadpointDisable(page: Page, index: number, action: "Disable" | "Enable") {
|
|
const target = page.getByTestId("loadpoint").nth(index);
|
|
const lpModal = page.getByTestId("loadpoint-modal");
|
|
await target.getByRole("button", { name: "edit" }).click();
|
|
await expectModalVisible(lpModal);
|
|
await lpModal.getByRole("button", { name: action }).click();
|
|
await expectModalHidden(lpModal);
|
|
}
|
|
|
|
test.describe("disable / enable", async () => {
|
|
test("loadpoint", async ({ page }) => {
|
|
autoAcceptDialogs(page);
|
|
await start();
|
|
await page.goto("/#/config");
|
|
|
|
// two loadpoints so disabling one keeps the site valid
|
|
await createLoadpoint(page, "Carport");
|
|
// reload between creations to reset modal state
|
|
await page.reload();
|
|
await createLoadpoint(page, "Garage");
|
|
|
|
// restart to bring both loadpoints live
|
|
await restart();
|
|
await page.reload();
|
|
|
|
// switch charge mode on dashboard; runtime setting is persisted next to the loadpoint config
|
|
await page.goto("/");
|
|
const modeButton = page
|
|
.getByTestId("loadpoint")
|
|
.nth(0)
|
|
.getByTestId("mode")
|
|
.getByRole("button", { name: "Fast" });
|
|
await modeButton.click();
|
|
await expect(modeButton).toHaveClass(/active/);
|
|
await page.goto("/#/config");
|
|
|
|
const target = page.getByTestId("loadpoint").nth(0);
|
|
|
|
await toggleLoadpointDisable(page, 0, "Disable");
|
|
|
|
// card shows disabled state
|
|
await expect(disabledBadge(target)).toBeVisible();
|
|
|
|
// restart, no fatal, both loadpoints still listed
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
|
|
await expect(disabledBadge(target)).toBeVisible();
|
|
|
|
// re-enable by clicking the disabled card
|
|
await disabledBadge(target).click();
|
|
await expect(disabledBadge(target)).toHaveCount(0);
|
|
|
|
// restart, no fatal
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(disabledBadge(target)).toHaveCount(0);
|
|
});
|
|
|
|
test("grid meter", async ({ page }) => {
|
|
autoAcceptDialogs(page);
|
|
await start();
|
|
await page.goto("/#/config");
|
|
|
|
const meterModal = page.getByTestId("meter-modal");
|
|
|
|
// add grid meter
|
|
await page.getByRole("button", { name: "Add grid meter" }).click();
|
|
await expectModalVisible(meterModal);
|
|
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
|
|
await meterModal.getByLabel("Power").fill("0");
|
|
await meterModal.getByRole("button", { name: "Save" }).click();
|
|
await expectModalHidden(meterModal);
|
|
|
|
const gridCard = page.getByTestId("grid");
|
|
|
|
// open modal, disable
|
|
await gridCard.getByRole("button", { name: "edit" }).click();
|
|
await expectModalVisible(meterModal);
|
|
await expect(meterModal.getByRole("button", { name: "Disable" })).toBeVisible();
|
|
await meterModal.getByRole("button", { name: "Disable" }).click();
|
|
await expectModalHidden(meterModal);
|
|
|
|
// card shows disabled state
|
|
await expect(disabledBadge(gridCard)).toBeVisible();
|
|
|
|
// restart, no fatal
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(disabledBadge(gridCard)).toBeVisible();
|
|
|
|
// re-enable by clicking the disabled card
|
|
await disabledBadge(gridCard).click();
|
|
await expect(disabledBadge(gridCard)).toHaveCount(0);
|
|
|
|
// restart, no fatal
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(disabledBadge(gridCard)).toHaveCount(0);
|
|
});
|
|
|
|
test("pv meter", async ({ page }) => {
|
|
autoAcceptDialogs(page);
|
|
await start();
|
|
await page.goto("/#/config");
|
|
|
|
const meterModal = page.getByTestId("meter-modal");
|
|
|
|
// add pv meter
|
|
await page.getByRole("button", { name: "Add solar or battery" }).click();
|
|
await expectModalVisible(meterModal);
|
|
await meterModal.getByRole("button", { name: "Add solar meter" }).click();
|
|
await meterModal.getByLabel("Manufacturer").selectOption("Demo meter");
|
|
await meterModal.getByLabel("Title").fill("PV");
|
|
await meterModal.getByLabel("Power").fill("0");
|
|
await meterModal.getByRole("button", { name: "Save" }).click();
|
|
await expectModalHidden(meterModal);
|
|
|
|
const pvCard = page.getByTestId("pv");
|
|
|
|
// open modal, disable
|
|
await pvCard.getByRole("button", { name: "edit" }).click();
|
|
await expectModalVisible(meterModal);
|
|
await expect(meterModal.getByRole("button", { name: "Disable" })).toBeVisible();
|
|
await meterModal.getByRole("button", { name: "Disable" }).click();
|
|
await expectModalHidden(meterModal);
|
|
|
|
// card shows disabled state
|
|
await expect(disabledBadge(pvCard)).toBeVisible();
|
|
|
|
// restart, no fatal
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(disabledBadge(pvCard)).toBeVisible();
|
|
|
|
// re-enable by clicking the disabled card
|
|
await disabledBadge(pvCard).click();
|
|
await expect(disabledBadge(pvCard)).toHaveCount(0);
|
|
|
|
// restart, no fatal
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(disabledBadge(pvCard)).toHaveCount(0);
|
|
});
|
|
|
|
test("grid tariff", async ({ page }) => {
|
|
autoAcceptDialogs(page);
|
|
await start();
|
|
await page.goto("/#/config");
|
|
|
|
const tariffModal = page.getByTestId("tariff-modal");
|
|
const gridCard = page.getByTestId("tariff-grid");
|
|
|
|
// add grid tariff
|
|
await page.getByRole("button", { name: "Add tariff" }).click();
|
|
await expectModalVisible(tariffModal);
|
|
await tariffModal.getByRole("button", { name: "Add grid import tariff" }).click();
|
|
await tariffModal.getByLabel("Provider").selectOption("Fixed Price");
|
|
await tariffModal.getByLabel("Price").fill("32.1");
|
|
await tariffModal.getByRole("button", { name: "Validate & save" }).click();
|
|
await expectModalHidden(tariffModal);
|
|
|
|
// open modal, disable
|
|
await gridCard.getByRole("button", { name: "edit" }).click();
|
|
await expectModalVisible(tariffModal);
|
|
await tariffModal.getByRole("button", { name: "Disable" }).click();
|
|
await expectModalHidden(tariffModal);
|
|
|
|
// card shows disabled state
|
|
await expect(disabledBadge(gridCard)).toBeVisible();
|
|
|
|
// restart, tariff not instantiated
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(disabledBadge(gridCard)).toBeVisible();
|
|
await expect(gridCard).not.toContainText("32.1");
|
|
|
|
// re-enable by clicking the disabled card
|
|
await disabledBadge(gridCard).click();
|
|
await expect(disabledBadge(gridCard)).toHaveCount(0);
|
|
|
|
// restart, tariff active again
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(disabledBadge(gridCard)).toHaveCount(0);
|
|
await expect(gridCard).toContainText(["Price", "32.1 ct"].join(""));
|
|
});
|
|
|
|
test("user-defined vehicle", async ({ page }) => {
|
|
autoAcceptDialogs(page);
|
|
await start();
|
|
await page.goto("/#/config");
|
|
|
|
// loadpoint so the vehicle shows up on the dashboard
|
|
await createLoadpoint(page, "Carport");
|
|
await page.reload();
|
|
|
|
// add user-defined vehicle
|
|
await page.getByTestId("add-vehicle").click();
|
|
const modal = page.getByTestId("vehicle-modal");
|
|
await expectModalVisible(modal);
|
|
await modal.getByLabel("Manufacturer").selectOption("User-defined device");
|
|
await page.waitForLoadState("networkidle");
|
|
const editor = modal.getByTestId("yaml-editor");
|
|
await editorClear(editor);
|
|
await editorPaste(
|
|
editor,
|
|
page,
|
|
`title: blue Honda
|
|
capacity: 12.3
|
|
soc:
|
|
source: const
|
|
value: 42`
|
|
);
|
|
await modal.getByRole("button", { name: "Save" }).click();
|
|
await expectModalHidden(modal);
|
|
|
|
const vehicleCard = page.getByTestId("vehicle");
|
|
await expect(vehicleCard).toHaveCount(1);
|
|
|
|
// open modal, disable
|
|
await vehicleCard.getByRole("button", { name: "edit" }).click();
|
|
await expectModalVisible(modal);
|
|
await modal.getByRole("button", { name: "Disable" }).click();
|
|
await expectModalHidden(modal);
|
|
|
|
// card shows disabled state
|
|
await expect(disabledBadge(vehicleCard)).toBeVisible();
|
|
|
|
// restart, no fatal
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(disabledBadge(vehicleCard)).toBeVisible();
|
|
|
|
// disabled vehicle is hidden from main ui
|
|
await page.goto("/");
|
|
await expect(page.getByTestId("loadpoint")).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("change-vehicle").getByRole("option", { name: "blue Honda" })
|
|
).toHaveCount(0);
|
|
|
|
// no vehicles entry in more menu
|
|
const moreTab = page.getByTestId("tab-more");
|
|
await moreTab.click();
|
|
await expect(moreTab.getByRole("button", { name: "User Interface" })).toBeVisible();
|
|
await expect(moreTab.getByRole("button", { name: "Vehicles" })).not.toBeVisible();
|
|
// close more menu
|
|
await moreTab.click();
|
|
|
|
await page.goto("/#/config");
|
|
|
|
// re-enable by clicking the disabled card
|
|
await disabledBadge(vehicleCard).click();
|
|
await expect(disabledBadge(vehicleCard)).toHaveCount(0);
|
|
|
|
// restart, yaml config intact
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(vehicleCard).toContainText("blue Honda");
|
|
|
|
// vehicle is selectable on the dashboard again
|
|
await page.goto("/");
|
|
await expect(
|
|
page.getByTestId("change-vehicle").getByRole("option", { name: "blue Honda" })
|
|
).toHaveCount(1);
|
|
|
|
// vehicle is back in the more menu vehicles modal
|
|
await moreTab.click();
|
|
await moreTab.getByRole("button", { name: "Vehicles" }).click();
|
|
const settingsModal = page.getByTestId("vehicle-settings-modal");
|
|
await expectModalVisible(settingsModal);
|
|
await expect(settingsModal.getByRole("group", { name: "blue Honda" })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe("disabled loadpoint behavior", async () => {
|
|
test("broken charger boots after disabling its loadpoint", async ({ page }) => {
|
|
autoAcceptDialogs(page);
|
|
await startSimulator();
|
|
await start();
|
|
await page.goto("/#/config");
|
|
|
|
// second loadpoint so the site stays valid
|
|
await createLoadpoint(page, "Carport");
|
|
await page.reload();
|
|
|
|
// loadpoint with shelly charger against simulator
|
|
await newLoadpoint(page, "Garage");
|
|
const lpModal = page.getByTestId("loadpoint-modal");
|
|
await lpModal.getByRole("button", { name: "Add charger" }).click();
|
|
const chargerModal = page.getByTestId("charger-modal");
|
|
await expectModalVisible(chargerModal);
|
|
await chargerModal.getByLabel("Manufacturer").selectOption("Shelly 1");
|
|
await chargerModal.getByLabel("IP address or hostname").fill(simulatorHost());
|
|
await chargerModal.getByRole("button", { name: "Validate & save" }).click();
|
|
await expectModalHidden(chargerModal);
|
|
await expectModalVisible(lpModal);
|
|
// charger save instant-creates the loadpoint
|
|
await finishLoadpoint(page);
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// break charger
|
|
await stopSimulator();
|
|
await restart();
|
|
await page.reload();
|
|
await expect(page.getByTestId("fatal-error")).toBeVisible();
|
|
|
|
// disable loadpoint with broken charger
|
|
await toggleLoadpointDisable(page, 1, "Disable");
|
|
await expect(disabledBadge(page.getByTestId("loadpoint").nth(1))).toBeVisible();
|
|
|
|
// clean boot, charger no longer instantiated
|
|
await restart();
|
|
await page.reload();
|
|
await expectNoFatal(page);
|
|
await expect(disabledBadge(page.getByTestId("loadpoint").nth(1))).toBeVisible();
|
|
|
|
// re-enable, fatal returns
|
|
await disabledBadge(page.getByTestId("loadpoint").nth(1)).click();
|
|
await expect(disabledBadge(page.getByTestId("loadpoint").nth(1))).toHaveCount(0);
|
|
await restart();
|
|
await page.reload();
|
|
await expect(page.getByTestId("fatal-error")).toBeVisible();
|
|
});
|
|
|
|
test("disabling a loadpoint keeps api indexes stable", async ({ page }) => {
|
|
autoAcceptDialogs(page);
|
|
await start();
|
|
await page.goto("/#/config");
|
|
|
|
await createLoadpoint(page, "Carport");
|
|
await page.reload();
|
|
await createLoadpoint(page, "Garage");
|
|
await page.reload();
|
|
await createLoadpoint(page, "Süd");
|
|
|
|
// disable middle loadpoint
|
|
await toggleLoadpointDisable(page, 1, "Disable");
|
|
await restart();
|
|
|
|
// main ui hides disabled loadpoint
|
|
await page.goto("/");
|
|
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
|
|
await expect(page.getByRole("heading", { name: "Garage" })).not.toBeVisible();
|
|
await expect(page.getByTestId("loadpoint").nth(0)).toContainText("Carport");
|
|
await expect(page.getByTestId("loadpoint").nth(1)).toContainText("Süd");
|
|
|
|
// state keeps disabled loadpoint at its position
|
|
const state = await (await page.request.get("/api/state")).json();
|
|
expect(state.loadpoints).toHaveLength(3);
|
|
expect(state.loadpoints[1].disabled).toBe(true);
|
|
expect(state.loadpoints[1].title).toBe("Garage");
|
|
|
|
// api index of third loadpoint does not shift
|
|
const res = await page.request.post("/api/loadpoints/3/mode/now");
|
|
expect(res.status()).toBe(200);
|
|
const updated = await (await page.request.get("/api/state")).json();
|
|
expect(updated.loadpoints[2].mode).toBe("now");
|
|
expect(updated.loadpoints[0].mode).not.toBe("now");
|
|
|
|
// disabled loadpoint has no api routes
|
|
const disabledRes = await page.request.post("/api/loadpoints/2/mode/now");
|
|
expect(disabledRes.status()).toBe(404);
|
|
|
|
// ui settings show disabled loadpoint as inactive row
|
|
const menu = await openMoreMenu(page);
|
|
await menu.getByRole("button", { name: "User Interface" }).click();
|
|
const modal = page.getByTestId("global-settings-modal");
|
|
await expectModalVisible(modal);
|
|
const garageRow = modal.getByRole("listitem", { name: "Draggable: Garage" });
|
|
await expect(garageRow).toContainText("disabled");
|
|
await expect(modal.getByRole("switch", { name: "Hide Garage" })).toHaveCount(0);
|
|
await expect(modal.getByRole("switch", { name: "Hide Carport" })).toBeEnabled();
|
|
|
|
// last-visible guard ignores disabled loadpoint
|
|
await modal.getByRole("switch", { name: "Hide Carport" }).click();
|
|
await expect(modal.getByRole("switch", { name: "Hide Süd" })).toBeDisabled();
|
|
});
|
|
|
|
test("editing a disabled loadpoint", async ({ page }) => {
|
|
autoAcceptDialogs(page);
|
|
await start();
|
|
await page.goto("/#/config");
|
|
|
|
await createLoadpoint(page, "Carport");
|
|
await page.reload();
|
|
await createLoadpoint(page, "Garage");
|
|
|
|
await toggleLoadpointDisable(page, 1, "Disable");
|
|
|
|
const target = page.getByTestId("loadpoint").nth(1);
|
|
const lpModal = page.getByTestId("loadpoint-modal");
|
|
|
|
// update title while disabled
|
|
await target.getByRole("button", { name: "edit" }).click();
|
|
await expectModalVisible(lpModal);
|
|
await lpModal.getByLabel("Title").fill("Garage 2");
|
|
await lpModal.getByRole("button", { name: "Save" }).click();
|
|
await expectModalHidden(lpModal);
|
|
await expect(target).toContainText("Garage 2");
|
|
|
|
// survives restart, still disabled
|
|
await restart();
|
|
await page.reload();
|
|
await expect(target).toContainText("Garage 2");
|
|
await expect(disabledBadge(target)).toBeVisible();
|
|
|
|
// re-enable, title applies to live loadpoint
|
|
await disabledBadge(target).click();
|
|
await expect(disabledBadge(target)).toHaveCount(0);
|
|
await restart();
|
|
await page.goto("/");
|
|
await expect(page.getByRole("heading", { name: "Garage 2" })).toBeVisible();
|
|
});
|
|
});
|