const { test, expect } = require("@playwright/test"); const username = process.env.OPNSENSE_UI_USERNAME; const password = process.env.OPNSENSE_UI_PASSWORD; async function closeDialog(dialog) { const hidden = dialog.evaluate( (element) => new Promise((resolve) => { window.jQuery(element).one("hidden.bs.modal", resolve); }), ); await dialog.getByRole("button", { name: "Close" }).click(); await hidden; await expect(dialog).toBeHidden(); } if (!username || !password) { throw new Error( "Set OPNSENSE_UI_USERNAME and OPNSENSE_UI_PASSWORD for the disposable OPNsense deployment.", ); } if (process.env.OPNSENSE_UI_TEST_ALLOW_MUTATION !== "1") { throw new Error( "Set OPNSENSE_UI_TEST_ALLOW_MUTATION=1 to allow enrollment changes in the disposable deployment.", ); } test("renders and preserves NordVPN enrollment controls", async ({ page }) => { const errors = []; page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); }); page.on("pageerror", (error) => errors.push(error.stack || error.message)); page.on("response", (response) => { if (response.status() >= 400) errors.push(`${response.status()} ${response.url()}`); }); await page.route("**/api/nordvpn/service/countries", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ status: "ok", countries: [ { code: "XA", name: "Synthetic A" }, { code: "XB", name: "Synthetic B" }, ], }), }); }); let startBootstrap; const bootstrapStarted = new Promise((resolve) => { startBootstrap = resolve; }); let finishBootstrap; await page.route("**/api/nordvpn/service/bootstrap", async (route) => { startBootstrap(); await new Promise((resolve) => { finishBootstrap = resolve; }); await route.fulfill({ contentType: "application/json", body: JSON.stringify({ status: "ok", tunnel: {} }), }); }); await page.goto("/"); await page.locator("#usernamefld").fill(username); await page.locator("#passwordfld").fill(password); await page.getByRole("button", { name: "Login" }).click(); await expect(page.locator("#usernamefld")).toBeHidden(); await page.goto("/ui/nordvpn"); await expect( page.getByRole("heading", { name: "NordVPN egress enrollment" }), ).toBeVisible(); const countries = page.locator('[id="settings.general.countries"]'); await expect(countries.locator('option[value="XA"]')).toHaveText( "Synthetic A", ); await expect(countries.locator('option[value="XB"]')).toHaveText( "Synthetic B", ); await expect( countries.locator("xpath=..").locator("button.dropdown-toggle"), ).toBeVisible(); const failurePolicy = page.locator('[id="settings.general.failure_policy"]'); await expect(failurePolicy.locator('option[value="fail_closed"]')).toHaveText( "Block traffic", ); await expect(failurePolicy.locator('option[value="fail_open"]')).toHaveText( "Use direct egress", ); const grid = page.getByRole("grid"); const testRows = grid.getByRole("row").filter({ hasText: "UI_TEST_NETWORK" }); while (await testRows.count()) { const before = await testRows.count(); await testRows.first().getByRole("button", { name: "Delete" }).click(); const removal = page.getByRole("dialog", { name: "Remove" }); await removal.getByRole("button", { name: "Yes" }).click(); await expect(testRows).toHaveCount(before - 1); } await page.getByRole("button", { name: "Add", exact: true }).click(); const dialog = page.locator(".modal:visible"); await expect(dialog).toBeVisible(); await dialog.locator('[id="enrollment.interfaces"]').selectOption("lan"); const source = dialog.locator('select[for="enrollment.source"]'); const destination = dialog.locator('select[for="enrollment.destination"]'); await expect(source).toBeVisible(); await expect(destination).toBeVisible(); await expect(source.locator('option[value="UI_TEST_NETWORK"]')).toHaveText( "UI_TEST_NETWORK", ); const destinationPicker = destination.locator("xpath=.."); const destinationToggle = destinationPicker.locator("button.dropdown-toggle"); await destinationToggle.click(); const destinationMenu = destinationPicker .locator(".dropdown-menu.open") .first(); await expect(destinationMenu).toBeVisible(); const menuBox = await destinationMenu.boundingBox(); const footerBox = await dialog.locator(".modal-footer").boundingBox(); expect(menuBox.y + menuBox.height).toBeLessThanOrEqual(footerBox.y); await destinationToggle.click(); await source.selectOption("UI_TEST_NETWORK"); await destination.selectOption("any"); await dialog.getByRole("button", { name: "Save", exact: true }).click(); await expect(dialog).toBeHidden(); await page.reload(); await expect( page.getByRole("heading", { name: "NordVPN egress enrollment" }), ).toBeVisible(); const row = page .getByRole("row") .filter({ hasText: "LAN" }) .filter({ hasText: "UI_TEST_NETWORK" }) .first(); await expect(row).toBeVisible(); await row.getByRole("button", { name: "Edit" }).click(); await expect(dialog).toBeVisible(); await expect(dialog.locator('select[for="enrollment.source"]')).toHaveValues([ "UI_TEST_NETWORK", ]); await expect( dialog.locator('select[for="enrollment.destination"]'), ).toHaveValues(["any"]); await closeDialog(dialog); await row.getByRole("button", { name: "Clone" }).click(); await expect(dialog).toBeVisible(); await expect(dialog.locator('select[for="enrollment.source"]')).toHaveValues([ "UI_TEST_NETWORK", ]); await expect( dialog.locator('select[for="enrollment.destination"]'), ).toHaveValues(["any"]); await closeDialog(dialog); const beforeDelete = await testRows.count(); await row.getByRole("button", { name: "Delete" }).click(); const removal = page.getByRole("dialog", { name: "Remove" }); await expect(removal).toBeVisible(); await removal.getByRole("button", { name: "Yes" }).click(); await expect(testRows).toHaveCount(beforeDelete - 1); const accessToken = page.locator("#accessToken"); const bootstrap = page.getByRole("button", { name: "Bootstrap NordLynx" }); await accessToken.fill("synthetic-token"); await bootstrap.click(); await bootstrapStarted; await expect(accessToken).toHaveValue(""); await expect(bootstrap).toBeDisabled(); finishBootstrap(); await expect(bootstrap).toBeEnabled(); await expect(page.locator("#bootstrapMsg")).toHaveText("Bootstrap started"); expect(errors).toEqual([]); });