chore: e2e stability (#22307)

This commit is contained in:
Michael Geers 2025-07-11 12:16:04 +02:00 • committed by GitHub
parent f64efb83ec
commit 7b3719e69f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 147 additions and 44 deletions

View file

@ -18,6 +18,7 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: "1.24.0"
cache: false # avoid cache thrashing
id: go
- uses: actions/cache@v4
@ -222,6 +223,7 @@ jobs:
- name: Run tests
run: npx playwright test
timeout-minutes: 20
env:
TZ: Europe/Berlin

View file

@ -1,6 +1,11 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { expectModalHidden, expectModalVisible } from "./utils";
import {
expectModalHidden,
expectModalVisible,
openTopNavigation,
expectTopNavigationClosed,
} from "./utils";
test.use({ baseURL: baseUrl() });
const BASIC = "basics.evcc.yaml";
@ -38,8 +43,9 @@ test("login", async ({ page }) => {
await page.goto("/");
// go to config
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("link", { name: "Configuration" }).click();
await expectTopNavigationClosed(page);
// login modal
const login = page.getByTestId("login-modal");
@ -65,8 +71,9 @@ test("http iframe hint", async ({ page }) => {
await page.goto("/");
// go to config
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("link", { name: "Configuration" }).click();
await expectTopNavigationClosed(page);
// login modal
const login = page.getByTestId("login-modal");
@ -116,13 +123,15 @@ test("update password", async ({ page }) => {
).not.toBeVisible();
// logout
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("button", { name: "Logout" }).click();
await expectTopNavigationClosed(page);
// login modal
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await expect(page.getByRole("button", { name: "Logout" })).not.toBeVisible();
await page.getByRole("link", { name: "Configuration" }).click();
await expectTopNavigationClosed(page);
const loginNew = page.getByTestId("login-modal");
await expectModalVisible(loginNew);
await loginNew.getByLabel("Password").fill(newPassword);
@ -153,8 +162,9 @@ test("disable auth", async ({ page }) => {
await expectModalHidden(modal);
// configuration page without login
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("link", { name: "Configuration" }).click();
await expectTopNavigationClosed(page);
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
await stop();

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { expectModalVisible, expectModalHidden } from "./utils";
import { expectModalVisible, expectModalHidden, openTopNavigation } from "./utils";
test.use({ baseURL: baseUrl() });
test.beforeAll(async () => {
@ -13,10 +13,11 @@ test.afterAll(async () => {
test.describe("battery settings", async () => {
test("open modal", async ({ page }) => {
await page.goto("/");
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByTestId("topnavigation-battery").click();
const modal = page.getByTestId("battery-settings-modal");
await expectModalVisible(modal);
await expect(modal.getByRole("heading", { name: "Home Battery" })).toBeVisible();
await expect(modal.getByRole("link", { name: "Grid charging 🧪" })).not.toBeVisible();
await expect(modal).toContainText("Battery level: 50%");
@ -25,20 +26,22 @@ test.describe("battery settings", async () => {
test("battery usage", async ({ page }) => {
await page.goto("/");
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByTestId("topnavigation-battery").click();
await page.locator("#batterySettingsPriority").selectOption({ label: "50%" });
await expect(page.locator("label[for=batterySettingsPriorityMiddle] span")).toHaveText("50%");
await expect(page.locator("label[for=batterySettingsPriorityBottom] span")).toHaveText("50%");
await page.locator("#batterySettingsBufferTop").selectOption({ label: "80%" });
await page.locator("#batterySettingsBufferStart").selectOption({ label: "when above 90%." });
await expect(page.locator("label[for=batterySettingsBuffer] span")).toHaveText("80%");
const modal = page.getByTestId("battery-settings-modal");
await expectModalVisible(modal);
await modal.locator("#batterySettingsPriority").selectOption({ label: "50%" });
await expect(modal.locator("label[for=batterySettingsPriorityMiddle] span")).toHaveText("50%");
await expect(modal.locator("label[for=batterySettingsPriorityBottom] span")).toHaveText("50%");
await modal.locator("#batterySettingsBufferTop").selectOption({ label: "80%" });
await modal.locator("#batterySettingsBufferStart").selectOption({ label: "when above 90%." });
await expect(modal.locator("label[for=batterySettingsBuffer] span")).toHaveText("80%");
});
test("grid charging", async ({ page }) => {
await page.goto("/");
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByTestId("topnavigation-battery").click();
const modal = page.getByTestId("battery-settings-modal");
await expectModalVisible(modal);

View file

@ -1,6 +1,12 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorUrl, simulatorHost } from "./simulator";
import {
startSimulator,
stopSimulator,
simulatorUrl,
simulatorHost,
simulatorApply,
} from "./simulator";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
const CONFIG_ONE_LP = "config-one-lp.evcc.yaml";
@ -29,7 +35,7 @@ test.describe("grid meter", async () => {
// setup test data for mock openems api
await page.goto(simulatorUrl());
await page.getByLabel("Grid Power").fill("5000");
await page.getByRole("button", { name: "Apply changes" }).click();
await simulatorApply(page);
await page.goto("/#/config");
await enableExperimental(page, false);

View file

@ -279,6 +279,8 @@ test.describe("charging loadpoint", async () => {
await addDemoCharger(page);
await lpModal.getByLabel("Default vehicle").selectOption(VEHICLE_2);
await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal);
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
// restart
await restart();
@ -320,6 +322,9 @@ test.describe("charging loadpoint", async () => {
await page.goto("/");
await expect(page.getByRole("button", { name: "Off" })).toHaveClass(/active/);
await page.getByRole("button", { name: "Solar", exact: true }).click();
await page.waitForLoadState("networkidle");
await expect(page.getByRole("button", { name: "Solar", exact: true })).toHaveClass(/active/);
await restart();
await page.reload();
await expect(page.getByRole("button", { name: "Solar", exact: true })).toHaveClass(/active/);

View file

@ -1,6 +1,12 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils";
import {
enableExperimental,
expectModalHidden,
expectModalVisible,
openTopNavigation,
expectTopNavigationClosed,
} from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
@ -16,8 +22,9 @@ test.afterAll(async () => {
test.describe("basics", async () => {
test("navigation to config", async ({ page }) => {
await page.goto("/");
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("link", { name: "Configuration" }).click();
await expectTopNavigationClosed(page);
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
});
});

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { expectModalHidden } from "./utils";
import { expectModalHidden, openTopNavigation, expectTopNavigationClosed } from "./utils";
import { ChildProcess } from "child_process";
test.use({ baseURL: baseUrl() });
@ -35,8 +35,9 @@ test.describe("demo mode", async () => {
});
test("auth is locked", async ({ page }) => {
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("link", { name: "Configuration" }).click();
await expectTopNavigationClosed(page);
const loginModal = page.getByTestId("login-modal");
await expect(loginModal).toBeVisible();
await expect(loginModal).toContainText("Login is not supported in demo mode.");

View file

@ -1,6 +1,12 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorUrl, simulatorConfig } from "./simulator";
import {
startSimulator,
stopSimulator,
simulatorUrl,
simulatorConfig,
simulatorApply,
} from "./simulator";
test.use({ baseURL: baseUrl() });
@ -18,7 +24,7 @@ test.beforeEach(async ({ page }) => {
await page.getByLabel("Grid Power").fill("500");
await page.getByTestId("vehicle0").getByLabel("SoC").fill("20");
await page.getByTestId("loadpoint0").getByText("B (connected)").click();
await page.getByRole("button", { name: "Apply changes" }).click();
await simulatorApply(page);
});
test.afterEach(async () => {
@ -41,7 +47,7 @@ test.describe("limitSoc", async () => {
test("can be set even if vehicle isn't connected yet", async ({ page }) => {
await page.goto(simulatorUrl());
await page.getByTestId("loadpoint0").getByText("A (disconnected)").click();
await page.getByRole("button", { name: "Apply changes" }).click();
await simulatorApply(page);
await page.goto("/");
await expect(page.getByTestId("vehicle-title")).toContainText("blauer e-Golf");
@ -52,7 +58,7 @@ test.describe("limitSoc", async () => {
await page.goto(simulatorUrl());
await page.getByTestId("loadpoint0").getByText("B (connected)").click();
await page.getByRole("button", { name: "Apply changes" }).click();
await simulatorApply(page);
await page.goto("/");
await expect(page.getByTestId("limit-soc-value")).toHaveText("50%");
@ -67,7 +73,7 @@ test.describe("limitSoc", async () => {
// disconnect
await page.goto(simulatorUrl());
await page.getByTestId("loadpoint0").getByText("A (disconnected)").click();
await page.getByRole("button", { name: "Apply changes" }).click();
await simulatorApply(page);
await page.goto("/");
await expect(page.getByTestId("vehicle-status")).toHaveText("Disconnected.");
@ -76,7 +82,7 @@ test.describe("limitSoc", async () => {
// connect
await page.goto(simulatorUrl());
await page.getByTestId("loadpoint0").getByText("B (connected)").click();
await page.getByRole("button", { name: "Apply changes" }).click();
await simulatorApply(page);
await page.goto("/");
await expect(page.getByTestId("vehicle-status")).toHaveText("Connected.");

View file

@ -1,5 +1,6 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { openTopNavigation, expectTopNavigationClosed } from "./utils";
test.use({ baseURL: baseUrl() });
@ -13,11 +14,19 @@ test.afterAll(async () => {
test.describe("opening logs", async () => {
test("via config", async ({ page }) => {
await page.goto("/");
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("link", { name: "Configuration" }).click();
await expectTopNavigationClosed(page);
await page.getByRole("link", { name: "Logs" }).click();
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
});
test("via top navigation", async ({ page }) => {
await page.goto("/");
await openTopNavigation(page);
await page.getByRole("link", { name: "Logs" }).click();
await expectTopNavigationClosed(page);
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
});
test("via notifications", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => window.app.raise({ message: "Fake Error" }));
@ -27,8 +36,9 @@ test.describe("opening logs", async () => {
});
test("via need help", async ({ page }) => {
await page.goto("/");
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("button", { name: "Need Help?" }).click();
await expectTopNavigationClosed(page);
await page.getByRole("link", { name: "View logs" }).click();
await expect(page.getByRole("heading", { name: "Logs", exact: false })).toBeVisible();
});

View file

@ -1,6 +1,7 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorConfig } from "./simulator";
import { openTopNavigation, expectTopNavigationClosed, closeTopNavigation } from "./utils";
const BASICS_CONFIG = "basics.evcc.yaml";
@ -28,7 +29,7 @@ test.describe("Basics", async () => {
await page.goto(route.path);
await expect(page.getByRole("heading", { name: route.title || title })).toBeVisible();
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await expect(page.getByRole("button", { name: "User Interface" })).toBeVisible();
await expect(page.getByRole("button", { name: "Home Battery" })).not.toBeVisible();
await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
@ -39,8 +40,9 @@ test.describe("Basics", async () => {
await page.goto("/");
await expect(page.getByRole("heading", { name: "Hello World" })).toBeVisible();
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("button", { name: "Need help?" }).click();
await expectTopNavigationClosed(page);
await expect(page.getByRole("heading", { name: "Need help?" })).toBeVisible();
});
@ -48,8 +50,9 @@ test.describe("Basics", async () => {
test("User Interface", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading", { name: "Hello World" })).toBeVisible();
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("button", { name: "User Interface" }).click();
await expectTopNavigationClosed(page);
await expect(page.getByRole("heading", { name: "User Interface" })).toBeVisible();
});
@ -73,18 +76,20 @@ test.describe("Advanced", async () => {
await page.goto(route.path);
await expect(page.getByRole("heading", { name: route.title || title })).toBeVisible();
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await expect(page.getByRole("button", { name: "User Interface" })).toBeVisible();
await expect(page.getByRole("button", { name: "Home Battery" })).toBeVisible();
await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
await closeTopNavigation(page);
}
});
test("Home Battery from top navigation", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading", { name: title })).toBeVisible();
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByRole("button", { name: "Home Battery" }).click();
await expectTopNavigationClosed(page);
await expect(page.getByRole("heading", { name: "Home Battery" })).toBeVisible();
});

View file

@ -1,6 +1,11 @@
import { test, expect, devices, type Page } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { expectModalVisible, expectModalHidden } from "./utils";
import {
expectModalVisible,
expectModalHidden,
openTopNavigation,
expectTopNavigationClosed,
} from "./utils";
test.use({ baseURL: baseUrl() });
@ -33,12 +38,12 @@ async function selectVehicleFilter(page: Page, value: string) {
test.describe("basics", async () => {
test("navigation to sessions", async ({ page }) => {
await page.goto("/");
const topNavigationButton = await page.getByTestId("topnavigation-button");
await topNavigationButton.click();
await openTopNavigation(page);
await page
.getByTestId("topnavigation-dropdown")
.getByRole("link", { name: "Charging sessions" })
.click();
await expectTopNavigationClosed(page);
await expect(page.getByRole("heading", { name: "Charging Sessions" })).toBeVisible();
});
test("month without data", async ({ page }) => {

View file

@ -5,6 +5,7 @@ import waitOn from "wait-on";
import axios from "axios";
import { spawn } from "child_process";
import { Transform } from "stream";
import type { Page } from "@playwright/test";
const LOG_ENABLED = false;
@ -80,3 +81,12 @@ export async function stopSimulator() {
log(`wait until port ${port} is closed`);
await waitOn({ resources: [`tcp:localhost:${port}`], reverse: true, log: LOG_ENABLED });
}
export const simulatorApply = async (page: Page) => {
await Promise.all([
page.waitForResponse(
(response) => response.url().includes("/api/state") && response.request().method() === "POST"
),
page.getByRole("button", { name: "Apply changes" }).click(),
]);
};

View file

@ -1,6 +1,12 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorUrl, simulatorConfig } from "./simulator";
import {
startSimulator,
stopSimulator,
simulatorUrl,
simulatorConfig,
simulatorApply,
} from "./simulator";
test.use({ baseURL: baseUrl() });
@ -19,7 +25,7 @@ test.beforeEach(async ({ page }) => {
await page.getByTestId("loadpoint0").getByLabel("Power").fill("6000");
await page.getByTestId("loadpoint0").getByText("C (charging)").click();
await page.getByTestId("loadpoint0").getByText("Enabled").check();
await page.getByRole("button", { name: "Apply changes" }).click();
await simulatorApply(page);
});
test.describe("smart cost limit", async () => {

View file

@ -4,8 +4,7 @@ export async function enableExperimental(page: Page, inline = true): Promise<voi
if (inline) {
await page.getByRole("button", { name: "Enable Experimental Features" }).click();
} else {
await expect(page.getByTestId("topnavigation-button")).toBeVisible();
await page.getByTestId("topnavigation-button").click();
await openTopNavigation(page);
await page.getByTestId("topnavigation-settings").click();
const modal = page.getByTestId("global-settings-modal");
await expectModalVisible(modal);
@ -16,6 +15,28 @@ export async function enableExperimental(page: Page, inline = true): Promise<voi
}
}
export async function openTopNavigation(page: Page): Promise<void> {
await expect(page.getByTestId("topnavigation-button")).toBeVisible();
await page.getByTestId("topnavigation-button").click();
await expectTopNavigationOpened(page);
}
export async function closeTopNavigation(page: Page): Promise<void> {
await expectTopNavigationOpened(page);
await page.getByTestId("topnavigation-button").click();
await expectTopNavigationClosed(page);
}
export async function expectTopNavigationOpened(page: Page): Promise<void> {
await expect(page.getByTestId("topnavigation-button")).toHaveAttribute("aria-expanded", "true");
await expect(page.getByTestId("topnavigation-dropdown")).toBeVisible();
}
export async function expectTopNavigationClosed(page: Page): Promise<void> {
await expect(page.getByTestId("topnavigation-button")).toHaveAttribute("aria-expanded", "false");
await expect(page.getByTestId("topnavigation-dropdown")).not.toBeVisible();
}
export async function expectModalVisible(modal: Locator): Promise<void> {
await expect(modal).toBeVisible();
await expect(modal).toHaveAttribute("aria-hidden", "false");

View file

@ -1,6 +1,12 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorUrl, simulatorConfig } from "./simulator";
import {
startSimulator,
stopSimulator,
simulatorUrl,
simulatorConfig,
simulatorApply,
} from "./simulator";
test.use({ baseURL: baseUrl() });
@ -18,7 +24,7 @@ test.beforeEach(async ({ page }) => {
await page.getByLabel("Grid Power").fill("500");
await page.getByTestId("vehicle0").getByLabel("SoC").fill("20");
await page.getByTestId("loadpoint0").getByText("B (connected)").click();
await page.getByRole("button", { name: "Apply changes" }).click();
await simulatorApply(page);
});
test.afterEach(async () => {