evcc-io/tests/config-ocpp.spec.ts

250 lines
11 KiB
TypeScript

import { test, expect, type Page } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorUrl, simulatorHost } from "./simulator";
import { expectModalVisible } from "./utils";
import axios from "axios";
test.use({ baseURL: baseUrl() });
const OCPP_STATION_ID = "test-station-001";
// open the OCPP modal on the evcc config page
async function openOcppModal(page: Page) {
await page.goto("/#/config");
const ocppModal = page.getByTestId("ocpp-modal");
await page.getByTestId("ocpp").getByRole("button", { name: "edit" }).click();
await expectModalVisible(ocppModal);
return ocppModal;
}
async function evccServerUrl(page: Page) {
const ocppModal = await openOcppModal(page);
return ocppModal.getByLabel("Server URL").inputValue();
}
// connect a charger to evcc via the simulator UI
async function connectCharger(page: Page, serverUrl: string) {
await page.goto(simulatorUrl());
const card = page.getByTestId("ocpp-add-client");
await card.getByLabel("Server URL").fill(serverUrl);
await card.getByLabel("Station ID").fill(OCPP_STATION_ID);
await card.getByRole("button", { name: "Connect" }).click();
}
// enable/disable the mock upstream OCPP server via the simulator UI. the toggle
// button name reflects the live state, so getByRole auto-waits past the initial fetch.
async function setMockServer(
page: Page,
opts: { enabled: boolean; username?: string; password?: string }
) {
await page.goto(simulatorUrl());
const server = page.getByTestId("ocpp-server");
if (opts.enabled) {
if (opts.username) await server.getByLabel("Username").fill(opts.username);
if (opts.password) await server.getByLabel("Password").fill(opts.password);
await server.getByRole("button", { name: "Enable server" }).click();
await expect(server.getByRole("button", { name: "Disable server" })).toBeVisible();
} else {
await server.getByRole("button", { name: "Disable server" }).click();
await expect(server.getByRole("button", { name: "Enable server" })).toBeVisible();
}
}
// open the forwarder editor for the given station (whatever the button state)
async function openForwarderEditor(page: Page, stationId: string) {
const ocppModal = await openOcppModal(page);
const station = ocppModal.getByTestId("ocpp-station").filter({ hasText: stationId });
await station.getByRole("button").click();
const forwarderModal = page.getByTestId("ocppforwarder-modal");
await expectModalVisible(forwarderModal);
return { ocppModal, station, forwarderModal };
}
// enable the mock upstream server, connect a charger, open the forwarder editor
async function startForwarding(page: Page, creds?: { username: string; password: string }) {
const serverUrl = await evccServerUrl(page);
await setMockServer(page, { enabled: true, ...creds });
await connectCharger(page, serverUrl);
return openForwarderEditor(page, OCPP_STATION_ID);
}
test.beforeEach(async () => {
await startSimulator();
await start();
});
test.afterEach(async () => {
await stop();
await stopSimulator();
});
test.describe("ocpp", () => {
test("ocpp modal", async ({ page }) => {
// Navigate to config page
await page.goto("/#/config");
// Open OCPP card
const ocppCard = page.getByTestId("ocpp");
await ocppCard.getByRole("button", { name: "edit" }).click();
const ocppModal = page.getByTestId("ocpp-modal");
await expectModalVisible(ocppModal);
const serverUrl = await ocppModal.getByLabel("Server URL").inputValue();
await expect(ocppModal).toContainText("No OCPP chargers detected.");
// Connect OCPP client via simulator UI
await page.goto(simulatorUrl());
// Fill in OCPP connection details
const addClientCard = page.getByTestId("ocpp-add-client");
await addClientCard.getByLabel("Server URL").fill(serverUrl);
await addClientCard.getByLabel("Station ID").fill(OCPP_STATION_ID);
await addClientCard.getByRole("button", { name: "Connect" }).click();
// Navigate back to evcc config page
await page.goto("/#/config");
await ocppCard.getByRole("button", { name: "edit" }).click();
await expectModalVisible(ocppModal);
await expect(ocppModal).toContainText("Station IDs");
const station = ocppModal.getByTestId("ocpp-station");
await expect(station).toContainText(OCPP_STATION_ID);
await expect(station.getByRole("img", { name: "Unknown" })).toBeVisible();
await expect(ocppModal).not.toContainText("No OCPP chargers detected.");
});
test("create ocpp charger", async ({ page }) => {
await page.goto("/#/config");
// Open loadpoint modal and select charging point type
await page.getByRole("button", { name: "Add charging point or heater" }).click();
const lpModal = page.getByTestId("loadpoint-modal");
await expectModalVisible(lpModal);
await lpModal.getByRole("button", { name: "Charging point" }).click();
await lpModal.getByLabel("Title").fill("OCPP Test Charger");
// Open charger modal and select OCPP 1.6J
await lpModal.getByRole("button", { name: "Add charger" }).click();
const chargerModal = page.getByTestId("charger-modal");
await expectModalVisible(chargerModal);
await chargerModal.getByLabel("Manufacturer").selectOption("OCPP 1.6J compatible");
// Verify waiting for connection state and extract server URL
const serverUrl = await chargerModal.getByLabel("OCPP-Server URL").inputValue();
const waitingButton = chargerModal.getByRole("button", { name: "Waiting for connection" });
await expect(waitingButton).toBeVisible();
// Connect OCPP client via simulator REST API
await axios.post(`${simulatorUrl()}/api/ocpp/connect`, {
stationId: OCPP_STATION_ID,
serverUrl: serverUrl,
});
// Verify connection successful
await expect(chargerModal).toContainText("Connected!");
await expect(waitingButton).not.toBeVisible();
// Proceed to validation step
await chargerModal.getByRole("button", { name: "Next step" }).click();
await expect(chargerModal.getByLabel("Station ID")).toHaveValue(OCPP_STATION_ID);
// Validate and verify sponsor token error
const testResult = chargerModal.getByTestId("test-result");
await testResult.getByRole("link", { name: "Validate" }).click();
await expect(testResult).toContainText("No sponsor token configured.");
});
});
test.describe("ocpp forwarder", () => {
test("unreachable upstream shows error", async ({ page }) => {
await connectCharger(page, await evccServerUrl(page));
const { ocppModal, station, forwarderModal } = await openForwarderEditor(page, OCPP_STATION_ID);
await forwarderModal.getByLabel("Upstream server URL").fill("ws://localhost:1/ocpp");
await forwarderModal.getByRole("button", { name: "Save" }).click();
// modal stays open; upstream is unreachable, so the error surfaces in place
await expectModalVisible(forwarderModal);
await expect(forwarderModal.getByTestId("ocppforwarder-error")).toBeVisible();
// remove the rule from the still-open modal
await forwarderModal.getByRole("button", { name: "Remove" }).click();
await expectModalVisible(ocppModal);
await expect(station.getByRole("button", { name: "No forwarding configured" })).toBeVisible();
});
test("connects and drops when upstream stops", async ({ page }) => {
const { forwarderModal } = await startForwarding(page);
await forwarderModal.getByLabel("Upstream server URL").fill(`ws://${simulatorHost()}`);
await forwarderModal.getByRole("button", { name: "Save" }).click();
await expect(forwarderModal.getByTestId("ocppforwarder-status")).toContainText("Connected");
// simulator UI shows our station as the active upstream connection
await page.goto(simulatorUrl());
const lastStation = page.getByTestId("ocpp-server-last-station");
await expect(lastStation).toContainText(OCPP_STATION_ID);
await expect(lastStation).toContainText("active");
// disabling the upstream server drops the forwarder connection
await setMockServer(page, { enabled: false });
const reopened = await openForwarderEditor(page, OCPP_STATION_ID);
await expect(reopened.forwarderModal.getByTestId("ocppforwarder-status")).toContainText(
"Not connected"
);
});
test("basic auth", async ({ page }) => {
const { forwarderModal } = await startForwarding(page, {
username: "user",
password: "secret",
});
await forwarderModal.getByLabel("Upstream server URL").fill(`ws://${simulatorHost()}`);
await forwarderModal.getByLabel("Username").fill("user");
await forwarderModal.getByLabel("Password").fill("secret");
await forwarderModal.getByRole("button", { name: "Save" }).click();
await expect(forwarderModal.getByTestId("ocppforwarder-status")).toContainText("Connected");
// simulator UI shows our station as the active upstream connection
await page.goto(simulatorUrl());
await expect(page.getByTestId("ocpp-server-last-station")).toContainText(OCPP_STATION_ID);
});
test("param change reconnects", async ({ page }) => {
const { forwarderModal } = await startForwarding(page, {
username: "user",
password: "secret",
});
const status = forwarderModal.getByTestId("ocppforwarder-status");
// wrong password is rejected
await forwarderModal.getByLabel("Upstream server URL").fill(`ws://${simulatorHost()}`);
await forwarderModal.getByLabel("Username").fill("user");
await forwarderModal.getByLabel("Password").fill("wrong");
await forwarderModal.getByRole("button", { name: "Save" }).click();
await expect(forwarderModal.getByTestId("ocppforwarder-error")).toBeVisible();
await expect(status).toContainText("Not connected");
// fixing the password re-establishes the connection
await forwarderModal.getByLabel("Password").fill("secret");
await forwarderModal.getByRole("button", { name: "Save" }).click();
await expect(status).toContainText("Connected");
});
test("removing rule stops forwarding", async ({ page }) => {
const { ocppModal, station, forwarderModal } = await startForwarding(page);
await forwarderModal.getByLabel("Upstream server URL").fill(`ws://${simulatorHost()}`);
await forwarderModal.getByRole("button", { name: "Save" }).click();
await expect(forwarderModal.getByTestId("ocppforwarder-status")).toContainText("Connected");
// removing the rule tears down forwarding
await forwarderModal.getByRole("button", { name: "Remove" }).click();
await expectModalVisible(ocppModal);
await expect(station.getByRole("button", { name: "No forwarding configured" })).toBeVisible();
// simulator UI no longer shows an active upstream connection
await page.goto(simulatorUrl());
await expect(page.getByTestId("ocpp-server-last-station")).not.toContainText("active");
});
});