diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 95dcaa5a8..de7f2441e 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -177,11 +177,6 @@ jobs: integration: name: Integration runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - shardIndex: [1, 2] - shardTotal: [2] steps: - uses: actions/checkout@v4 @@ -224,7 +219,7 @@ jobs: run: npx playwright install --with-deps chromium - name: Run tests - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: npx playwright test # - name: Run tests # uses: docker://mcr.microsoft.com/playwright:v1.34.3-jammy @@ -234,6 +229,6 @@ jobs: - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: - name: playwright-report-${{ matrix.shardIndex }} + name: playwright-report path: playwright-report/ retention-days: 14 diff --git a/playwright.config.js b/playwright.config.js index 17932c99d..c10ac944d 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -4,22 +4,21 @@ import { defineConfig, devices } from "@playwright/test"; * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: "./tests", - fullyParallel: false, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - timeout: 15000, // 15s (default 30s) - expect: { timeout: 2500 }, // 2.5s (default 5s) - workers: 1, // run testfiles serially to avoid port and database conflicts - reporter: "html", - use: { - baseURL: "http://127.0.0.1:7070", - trace: "on-first-retry", - }, - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], + testDir: "./tests", + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + timeout: 15000, // 15s (default 30s) + expect: { timeout: 2500 }, // 2.5s (default 5s) + workers: process.env.CI ? 3 : 8, + reporter: "html", + use: { + baseURL: "http://127.0.0.1:7070", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], }); diff --git a/tests/auth.spec.js b/tests/auth.spec.js index 23620d8fb..e7163cff5 100644 --- a/tests/auth.spec.js +++ b/tests/auth.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeEach(async ({ page }) => { await start("basics.evcc.yaml"); diff --git a/tests/basics.spec.js b/tests/basics.spec.js index 490d7c795..4a8504143 100644 --- a/tests/basics.spec.js +++ b/tests/basics.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start("basics.evcc.yaml", "password.sql"); diff --git a/tests/config-meters.spec.js b/tests/config-meters.spec.js new file mode 100644 index 000000000..fa3823429 --- /dev/null +++ b/tests/config-meters.spec.js @@ -0,0 +1,85 @@ +import { test, expect } from "@playwright/test"; +import { start, stop, restart, baseUrl } from "./evcc"; +import { startSimulator, stopSimulator, simulatorUrl, simulatorHost } from "./simulator"; + +const CONFIG_EMPTY = "config-empty.evcc.yaml"; + +test.use({ baseURL: baseUrl() }); + +test.beforeAll(async () => { + await start(CONFIG_EMPTY, "password.sql"); + await startSimulator(); +}); +test.afterAll(async () => { + await stop(); + await stopSimulator(); +}); + +async function login(page) { + await page.locator("#loginPassword").fill("secret"); + await page.getByRole("button", { name: "Login" }).click(); +} + +async function enableExperimental(page) { + await page + .getByTestId("generalconfig-experimental") + .getByRole("link", { name: "change" }) + .click(); + await page.getByLabel("Experimental 🧪").click(); + await page.getByRole("button", { name: "Close" }).click(); +} + +test.describe("meters", async () => { + test("create, edit and remove battery meter", async ({ page }) => { + // setup test data for mock openems api + await page.goto(simulatorUrl()); + await page.getByLabel("Battery Power").fill("-2500"); + await page.getByLabel("Battery SoC").fill("75"); + await page.getByRole("button", { name: "Apply changes" }).click(); + + await page.goto("/#/config"); + await login(page); + 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("Manufacturer").selectOption("OpenEMS"); + await meterModal.getByLabel("IP address or hostname").fill(simulatorHost()); + await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible(); + await meterModal.getByRole("link", { name: "validate" }).click(); + await expect(meterModal.getByText("SoC: 75.0%")).toBeVisible(); + await expect(meterModal.getByText("Power: -2.5 kW")).toBeVisible(); + await meterModal.getByRole("button", { name: "Save" }).click(); + await expect(page.getByTestId("battery")).toBeVisible(1); + await expect(page.getByTestId("battery")).toContainText("openems"); + + // edit #1 + await page.getByTestId("battery").getByRole("button", { name: "edit" }).click(); + await meterModal.getByLabel("Battery capacity in kWh").fill("20"); + await meterModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("battery")).toBeVisible(1); + await expect(page.getByTestId("battery")).toContainText("openems"); + await expect(page.getByTestId("battery").getByText("SoC: 75.0%")).toBeVisible(); + await expect(page.getByTestId("battery").getByText("Power: -2.5 kW")).toBeVisible(); + await expect(page.getByTestId("battery").getByText("Capacity: 20.0 kWh")).toBeVisible(); + + // restart and check in main ui + await restart(CONFIG_EMPTY); + await page.goto("/"); + await page.getByTestId("visualization").click(); + await expect(page.getByTestId("energyflow")).toContainText("Battery charging75%2.5 kW"); + + // delete #1 + await page.goto("/#/config"); + await page.getByTestId("battery").getByRole("button", { name: "edit" }).click(); + await meterModal.getByRole("button", { name: "Delete" }).click(); + + await expect(page.getByTestId("battery")).toHaveCount(0); + }); +}); diff --git a/tests/config-vehicles.spec.js b/tests/config-vehicles.spec.js new file mode 100644 index 000000000..5e7e4c5e4 --- /dev/null +++ b/tests/config-vehicles.spec.js @@ -0,0 +1,132 @@ +import { test, expect } from "@playwright/test"; +import { start, stop, restart, cleanRestart, baseUrl } from "./evcc"; + +const CONFIG_EMPTY = "config-empty.evcc.yaml"; +const CONFIG_WITH_VEHICLE = "config-with-vehicle.evcc.yaml"; + +test.use({ baseURL: baseUrl() }); + +test.beforeAll(async () => { + await start(CONFIG_EMPTY, "password.sql"); +}); +test.afterAll(async () => { + await stop(); +}); + +async function login(page) { + await page.locator("#loginPassword").fill("secret"); + await page.getByRole("button", { name: "Login" }).click(); +} + +async function enableExperimental(page) { + await page + .getByTestId("generalconfig-experimental") + .getByRole("link", { name: "change" }) + .click(); + await page.getByLabel("Experimental 🧪").click(); + await page.getByRole("button", { name: "Close" }).click(); +} + +test.describe("vehicles", async () => { + test("create, edit and delete vehicles", async ({ page }) => { + await page.goto("/#/config"); + await login(page); + await enableExperimental(page); + + await expect(page.getByTestId("vehicle")).toHaveCount(0); + const vehicleModal = page.getByTestId("vehicle-modal"); + + // create #1 + await page.getByTestId("add-vehicle").click(); + await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); + await vehicleModal.getByLabel("Title").fill("Green Car"); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(1); + + // create #2 + await page.getByTestId("add-vehicle").click(); + await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); + await vehicleModal.getByLabel("Title").fill("Yellow Van"); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(2); + await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Green Car/); + await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Yellow Van/); + + // edit #1 + await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); + await expect(vehicleModal.getByLabel("Title")).toHaveValue("Green Car"); + await vehicleModal.getByLabel("Title").fill("Fancy Car"); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(2); + await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Fancy Car/); + + // delete #1 + await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); + await vehicleModal.getByRole("button", { name: "Delete" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(1); + await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Yellow Van/); + + // delete #2 + await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); + await vehicleModal.getByRole("button", { name: "Delete" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(0); + }); + + test("config should survive restart", async ({ page }) => { + await page.goto("/#/config"); + await login(page); + await enableExperimental(page); + + await expect(page.getByTestId("vehicle")).toHaveCount(0); + const vehicleModal = page.getByTestId("vehicle-modal"); + + // create #1 & #2 + await page.getByTestId("add-vehicle").click(); + await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); + await vehicleModal.getByLabel("Title").fill("Green Car"); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await page.getByTestId("add-vehicle").click(); + await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); + await vehicleModal.getByLabel("Title").fill("Yellow Van"); + await vehicleModal.getByLabel("car").click(); + await vehicleModal.getByLabel("van").check(); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(2); + + // restart evcc + await restart(CONFIG_EMPTY); + await page.reload(); + + await expect(page.getByTestId("vehicle")).toHaveCount(2); + await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Green Car/); + await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Yellow Van/); + }); + + test("mixed config (yaml + db)", async ({ page }) => { + await cleanRestart(CONFIG_WITH_VEHICLE, "password.sql"); + + await page.goto("/#/config"); + await login(page); + await enableExperimental(page); + + await expect(page.getByTestId("vehicle")).toHaveCount(1); + const vehicleModal = page.getByTestId("vehicle-modal"); + + // create #1 + await page.getByTestId("add-vehicle").click(); + await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); + await vehicleModal.getByLabel("Title").fill("Green Car"); + await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); + + await expect(page.getByTestId("vehicle")).toHaveCount(2); + await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/YAML Bike/); + await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Green Car/); + }); +}); diff --git a/tests/config.spec.js b/tests/config.spec.js index 6edeccb0f..15a9da81a 100644 --- a/tests/config.spec.js +++ b/tests/config.spec.js @@ -1,9 +1,9 @@ import { test, expect } from "@playwright/test"; -import { start, stop, restart, cleanRestart } from "./evcc"; -import { startSimulator, stopSimulator, SIMULATOR_URL, SIMULATOR_HOST } from "./simulator"; +import { start, stop, baseUrl } from "./evcc"; const CONFIG_EMPTY = "config-empty.evcc.yaml"; -const CONFIG_WITH_VEHICLE = "config-with-vehicle.evcc.yaml"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start(CONFIG_EMPTY, "password.sql"); @@ -42,172 +42,6 @@ test.describe("basics", async () => { }); }); -test.describe("vehicles", async () => { - test("create, edit and delete vehicles", async ({ page }) => { - await page.goto("/#/config"); - await login(page); - await enableExperimental(page); - - await expect(page.getByTestId("vehicle")).toHaveCount(0); - const vehicleModal = page.getByTestId("vehicle-modal"); - - // create #1 - await page.getByTestId("add-vehicle").click(); - await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); - await vehicleModal.getByLabel("Title").fill("Green Car"); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(1); - - // create #2 - await page.getByTestId("add-vehicle").click(); - await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); - await vehicleModal.getByLabel("Title").fill("Yellow Van"); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(2); - await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Green Car/); - await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Yellow Van/); - - // edit #1 - await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); - await expect(vehicleModal.getByLabel("Title")).toHaveValue("Green Car"); - await vehicleModal.getByLabel("Title").fill("Fancy Car"); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(2); - await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Fancy Car/); - - // delete #1 - await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); - await vehicleModal.getByRole("button", { name: "Delete" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(1); - await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Yellow Van/); - - // delete #2 - await page.getByTestId("vehicle").nth(0).getByRole("button", { name: "edit" }).click(); - await vehicleModal.getByRole("button", { name: "Delete" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(0); - }); - - test("config should survive restart", async ({ page }) => { - await page.goto("/#/config"); - await login(page); - await enableExperimental(page); - - await expect(page.getByTestId("vehicle")).toHaveCount(0); - const vehicleModal = page.getByTestId("vehicle-modal"); - - // create #1 & #2 - await page.getByTestId("add-vehicle").click(); - await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); - await vehicleModal.getByLabel("Title").fill("Green Car"); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await page.getByTestId("add-vehicle").click(); - await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); - await vehicleModal.getByLabel("Title").fill("Yellow Van"); - await vehicleModal.getByLabel("car").click(); - await vehicleModal.getByLabel("van").check(); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(2); - - // restart evcc - await restart(CONFIG_EMPTY); - await page.reload(); - - await expect(page.getByTestId("vehicle")).toHaveCount(2); - await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/Green Car/); - await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Yellow Van/); - }); - - test("mixed config (yaml + db)", async ({ page }) => { - await cleanRestart(CONFIG_WITH_VEHICLE, "password.sql"); - - await page.goto("/#/config"); - await login(page); - await enableExperimental(page); - - await expect(page.getByTestId("vehicle")).toHaveCount(1); - const vehicleModal = page.getByTestId("vehicle-modal"); - - // create #1 - await page.getByTestId("add-vehicle").click(); - await vehicleModal.getByLabel("Manufacturer").selectOption("Generic vehicle"); - await vehicleModal.getByLabel("Title").fill("Green Car"); - await vehicleModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("vehicle")).toHaveCount(2); - await expect(page.getByTestId("vehicle").nth(0)).toHaveText(/YAML Bike/); - await expect(page.getByTestId("vehicle").nth(1)).toHaveText(/Green Car/); - }); -}); - -test.describe("meters", async () => { - test.beforeAll(async () => { - await startSimulator(); - }); - test.afterAll(async () => { - await stopSimulator(); - }); - - test("create, edit and remove battery meter", async ({ page }) => { - // setup test data for mock openems api - await page.goto(SIMULATOR_URL); - await page.getByLabel("Battery Power").fill("-2500"); - await page.getByLabel("Battery SoC").fill("75"); - await page.getByRole("button", { name: "Apply changes" }).click(); - - await page.goto("/#/config"); - await login(page); - 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("Manufacturer").selectOption("OpenEMS"); - await meterModal.getByLabel("IP address or hostname").fill(SIMULATOR_HOST); - await expect(meterModal.getByRole("button", { name: "Validate & save" })).toBeVisible(); - await meterModal.getByRole("link", { name: "validate" }).click(); - await expect(meterModal.getByText("SoC: 75.0%")).toBeVisible(); - await expect(meterModal.getByText("Power: -2.5 kW")).toBeVisible(); - await meterModal.getByRole("button", { name: "Save" }).click(); - await expect(page.getByTestId("battery")).toBeVisible(1); - await expect(page.getByTestId("battery")).toContainText("openems"); - - // edit #1 - await page.getByTestId("battery").getByRole("button", { name: "edit" }).click(); - await meterModal.getByLabel("Battery capacity in kWh").fill("20"); - await meterModal.getByRole("button", { name: "Validate & save" }).click(); - - await expect(page.getByTestId("battery")).toBeVisible(1); - await expect(page.getByTestId("battery")).toContainText("openems"); - await expect(page.getByTestId("battery").getByText("SoC: 75.0%")).toBeVisible(); - await expect(page.getByTestId("battery").getByText("Power: -2.5 kW")).toBeVisible(); - await expect(page.getByTestId("battery").getByText("Capacity: 20.0 kWh")).toBeVisible(); - - // restart and check in main ui - await restart(CONFIG_EMPTY); - await page.goto("/"); - await page.getByTestId("visualization").click(); - await expect(page.getByTestId("energyflow")).toContainText("Battery charging75%2.5 kW"); - - // delete #1 - await page.goto("/#/config"); - await page.getByTestId("battery").getByRole("button", { name: "edit" }).click(); - await meterModal.getByRole("button", { name: "Delete" }).click(); - - await expect(page.getByTestId("battery")).toHaveCount(0); - }); -}); - test.describe("general", async () => { test("change site title", async ({ page }) => { // initial value on main ui diff --git a/tests/evcc.js b/tests/evcc.js index 906c30068..b8ef6dd40 100644 --- a/tests/evcc.js +++ b/tests/evcc.js @@ -2,13 +2,25 @@ import fs from "fs"; import waitOn from "wait-on"; import axios from "axios"; import { exec, execSync } from "child_process"; -import playwrightConfig from "../playwright.config.js"; +import os from "os"; +import path from "path"; -const BASE_URL = playwrightConfig.use.baseURL; - -const DB_PATH = "./evcc.db"; const BINARY = "./evcc"; +function port() { + const index = process.env.TEST_WORKER_INDEX * 1; + return 11000 + index; +} + +export function baseUrl() { + return `http://localhost:${port()}`; +} + +function dbPath() { + const file = `evcc-${port()}.db`; + return path.join(os.tmpdir(), file); +} + export async function start(config, sqlDumps) { await _clean(); if (sqlDumps) { @@ -39,14 +51,17 @@ export async function cleanRestart(config, sqlDumps) { async function _restoreDatabase(sqlDumps) { const dumps = Array.isArray(sqlDumps) ? sqlDumps : [sqlDumps]; for (const dump of dumps) { - console.log("loading database", dump); - execSync(`sqlite3 ${DB_PATH} < tests/${dump}`); + console.log("loading database", dbPath(), dump); + execSync(`sqlite3 ${dbPath()} < tests/${dump}`); } } async function _start(config) { + const configFile = config.includes("/") ? config : `tests/${config}`; console.log("starting evcc", { config }); - const instance = exec(`EVCC_DATABASE_DSN=${DB_PATH} ${BINARY} --config tests/${config}`); + const instance = exec( + `EVCC_NETWORK_PORT=${port()} EVCC_DATABASE_DSN=${dbPath()} ${BINARY} --config ${configFile}` + ); instance.stdout.pipe(process.stdout); instance.stderr.pipe(process.stderr); instance.on("exit", (code) => { @@ -54,20 +69,21 @@ async function _start(config) { throw new Error("evcc terminated", code); } }); - await waitOn({ resources: [BASE_URL] }); + await waitOn({ resources: [baseUrl()] }); } async function _stop() { console.log("shutting down evcc"); - const res = await axios.post(BASE_URL + "/api/auth/login", { password: "secret" }); + const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" }); console.log(res.status, res.statusText); const cookie = res.headers["set-cookie"]; - await axios.post(BASE_URL + "/api/system/shutdown", {}, { headers: { cookie } }); + await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } }); } async function _clean() { - if (fs.existsSync(DB_PATH)) { - console.log("delete database", DB_PATH); - fs.unlinkSync(DB_PATH); + const db = dbPath(); + if (fs.existsSync(db)) { + console.log("delete database", db); + fs.unlinkSync(db); } } diff --git a/tests/heating.spec.js b/tests/heating.spec.js index 4cafc8322..d11df1968 100644 --- a/tests/heating.spec.js +++ b/tests/heating.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start("heating.evcc.yaml", "password.sql"); diff --git a/tests/limits.spec.js b/tests/limits.spec.js index e7244f712..520cfcb3d 100644 --- a/tests/limits.spec.js +++ b/tests/limits.spec.js @@ -1,8 +1,8 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; -import { startSimulator, stopSimulator, SIMULATOR_URL } from "./simulator"; +import { start, stop, baseUrl } from "./evcc"; +import { startSimulator, stopSimulator, simulatorUrl, simulatorConfig } from "./simulator"; -const CONFIG = "simulator.evcc.yaml"; +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await startSimulator(); @@ -12,9 +12,9 @@ test.afterAll(async () => { }); test.beforeEach(async ({ page }) => { - await start(CONFIG, "password.sql"); + await start(simulatorConfig(), "password.sql"); - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByLabel("Grid Power").fill("500"); await page.getByTestId("vehicle0").getByLabel("SoC").fill("20"); await page.getByTestId("loadpoint0").getByText("B (connected)").click(); @@ -39,7 +39,7 @@ test.describe("limitSoc", async () => { }); test("can be set even if vehicle isn't connected yet", async ({ page }) => { - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByTestId("loadpoint0").getByText("A (disconnected)").click(); await page.getByRole("button", { name: "Apply changes" }).click(); @@ -50,7 +50,7 @@ test.describe("limitSoc", async () => { await page.getByTestId("limit-soc").getByRole("combobox").selectOption("50%"); await expect(page.getByTestId("limit-soc-value")).toHaveText("50%"); - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByTestId("loadpoint0").getByText("B (connected)").click(); await page.getByRole("button", { name: "Apply changes" }).click(); @@ -65,7 +65,7 @@ test.describe("limitSoc", async () => { await expect(page.getByTestId("limit-soc-value")).toHaveText("50%"); // disconnect - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByTestId("loadpoint0").getByText("A (disconnected)").click(); await page.getByRole("button", { name: "Apply changes" }).click(); @@ -74,7 +74,7 @@ test.describe("limitSoc", async () => { await expect(page.getByTestId("limit-soc-value")).toHaveText("100%"); // connect - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByTestId("loadpoint0").getByText("B (connected)").click(); await page.getByRole("button", { name: "Apply changes" }).click(); diff --git a/tests/logs.spec.js b/tests/logs.spec.js index 91bd0d902..867ec493d 100644 --- a/tests/logs.spec.js +++ b/tests/logs.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start("basics.evcc.yaml", "password.sql"); diff --git a/tests/modals.spec.js b/tests/modals.spec.js index 62f229327..7d6fe553a 100644 --- a/tests/modals.spec.js +++ b/tests/modals.spec.js @@ -1,12 +1,13 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; -import { startSimulator, stopSimulator } from "./simulator"; +import { start, stop, baseUrl } from "./evcc"; +import { startSimulator, stopSimulator, simulatorConfig } from "./simulator"; const BASICS_CONFIG = "basics.evcc.yaml"; -const SIMULATOR_CONFIG = "simulator.evcc.yaml"; const UI_ROUTES = ["/", "/#/sessions", "/#/config"]; +test.use({ baseURL: baseUrl() }); + async function login(page) { await page.locator("#loginPassword").fill("secret"); await page.getByRole("button", { name: "Login" }).click(); @@ -55,7 +56,7 @@ test.describe("Basics", async () => { test.describe("Advanced", async () => { test.beforeAll(async () => { - await start(SIMULATOR_CONFIG, "password.sql"); + await start(simulatorConfig(), "password.sql"); await startSimulator(); }); diff --git a/tests/plan.spec.js b/tests/plan.spec.js index db0e0ce71..2836339bf 100644 --- a/tests/plan.spec.js +++ b/tests/plan.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); const CONFIG = "plan.evcc.yaml"; diff --git a/tests/sessions.spec.js b/tests/sessions.spec.js index 20f98bb2e..e46c6d6bb 100644 --- a/tests/sessions.spec.js +++ b/tests/sessions.spec.js @@ -1,5 +1,7 @@ import { test, expect, devices } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); const mobile = devices["iPhone 12 Mini"].viewport; const desktop = devices["Desktop Chrome"].viewport; diff --git a/tests/simulator.js b/tests/simulator.js index 27d91e2c2..8e8ad7b8e 100644 --- a/tests/simulator.js +++ b/tests/simulator.js @@ -1,15 +1,36 @@ +import os from "os"; +import path from "path"; +import fs from "fs"; import waitOn from "wait-on"; import axios from "axios"; import { exec } from "child_process"; -export const SIMULATOR_HOST = "localhost:7072"; -export const SIMULATOR_URL = `http://${SIMULATOR_HOST}/`; -const HEALTH_URL = SIMULATOR_URL + "api/state"; -const SHUTDOWN_URL = SIMULATOR_URL + "api/shutdown"; +function port() { + const index = process.env.TEST_PARALLEL_INDEX * 1; + return 12000 + index; +} + +export function simulatorHost() { + return `localhost:${port()}`; +} + +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() { console.log("starting simulator"); - const instance = exec("npm run simulator"); + const instance = exec(`npm run simulator -- --port ${port()}`); console.log("exec end"); instance.stdout.pipe(process.stdout); instance.stderr.pipe(process.stderr); @@ -20,10 +41,10 @@ export async function startSimulator() { } }); console.log("waiton"); - await waitOn({ resources: [HEALTH_URL], log: true }); + await waitOn({ resources: [`${simulatorUrl()}/api/state`], log: true }); } export async function stopSimulator() { console.log("shutting down simulator"); - await axios.post(SHUTDOWN_URL); + await axios.post(`${simulatorUrl()}/api/shutdown`); } diff --git a/tests/smart-cost.spec.js b/tests/smart-cost.spec.js index dc2dd845b..8e62838da 100644 --- a/tests/smart-cost.spec.js +++ b/tests/smart-cost.spec.js @@ -1,11 +1,11 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; -import { startSimulator, stopSimulator, SIMULATOR_URL } from "./simulator"; +import { start, stop, baseUrl } from "./evcc"; +import { startSimulator, stopSimulator, simulatorUrl, simulatorConfig } from "./simulator"; -const CONFIG = "simulator.evcc.yaml"; +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { - await start(CONFIG, "password.sql"); + await start(simulatorConfig(), "password.sql"); await startSimulator(); }); test.afterAll(async () => { @@ -14,7 +14,7 @@ test.afterAll(async () => { }); test.beforeEach(async ({ page }) => { - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByLabel("PV Power").fill("6000"); await page.getByTestId("loadpoint0").getByLabel("Power").fill("6000"); await page.getByTestId("loadpoint0").getByText("C (charging)").click(); diff --git a/tests/statistics.spec.js b/tests/statistics.spec.js index 249517505..33d63a6d9 100644 --- a/tests/statistics.spec.js +++ b/tests/statistics.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start("statistics.evcc.yaml", ["password.sql", "statistics.sql"]); diff --git a/tests/vehicle-error.spec.js b/tests/vehicle-error.spec.js index 381655f71..9a58a80a9 100644 --- a/tests/vehicle-error.spec.js +++ b/tests/vehicle-error.spec.js @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import { start, stop } from "./evcc"; +import { start, stop, baseUrl } from "./evcc"; + +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await start("vehicle-error.evcc.yaml", "password.sql"); diff --git a/tests/vehicle-settings.spec.js b/tests/vehicle-settings.spec.js index a02f8375c..52adfb7f9 100644 --- a/tests/vehicle-settings.spec.js +++ b/tests/vehicle-settings.spec.js @@ -1,8 +1,8 @@ import { test, expect } from "@playwright/test"; -import { start, stop, restart } from "./evcc"; -import { startSimulator, stopSimulator, SIMULATOR_URL } from "./simulator"; +import { start, stop, restart, baseUrl } from "./evcc"; +import { startSimulator, stopSimulator, simulatorUrl, simulatorConfig } from "./simulator"; -const CONFIG = "simulator.evcc.yaml"; +test.use({ baseURL: baseUrl() }); test.beforeAll(async () => { await startSimulator(); @@ -12,9 +12,9 @@ test.afterAll(async () => { }); test.beforeEach(async ({ page }) => { - await start(CONFIG, "password.sql"); + await start(simulatorConfig(), "password.sql"); - await page.goto(SIMULATOR_URL); + await page.goto(simulatorUrl()); await page.getByLabel("Grid Power").fill("500"); await page.getByTestId("vehicle0").getByLabel("SoC").fill("20"); await page.getByTestId("loadpoint0").getByText("B (connected)").click(); @@ -36,7 +36,7 @@ test.describe("minSoc", async () => { await page.getByRole("combobox", { name: "Min. charge %" }).selectOption("20%"); await expect(page.getByText("charged to 20% in solar mode")).toBeVisible(); - await restart(CONFIG); + await restart(simulatorConfig()); await page.reload(); await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click(); @@ -75,7 +75,7 @@ test.describe("limitSoc", async () => { await page.getByRole("button", { name: "Close" }).click(); await expect(page.getByTestId("limit-soc-value")).toContainText("80%"); - await restart(CONFIG); + await restart(simulatorConfig()); await page.reload(); await expect(page.getByTestId("limit-soc-value")).toContainText("80%");