evcc-io/tests/plan.spec.ts
2025-06-26 14:36:29 +00:00

634 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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").nth(2);
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();
});
});