From 3d9bbe1a628a1a174bf6ca90c626cf4a1d2d72f8 Mon Sep 17 00:00:00 2001 From: Shaun Date: Wed, 28 Aug 2024 17:18:04 -0400 Subject: [PATCH] Improve Playwright/E2E testing setup (#5056) Co-authored-by: Frank Elsinga --- config/playwright.config.js | 12 ++++-- server/server.js | 30 +++++++++++++++ test/e2e/setup.spec.js | 43 --------------------- test/e2e/specs/example.spec.js | 38 +++++++++++++++++++ test/e2e/specs/setup-process.once.js | 56 ++++++++++++++++++++++++++++ test/e2e/util-test.js | 35 +++++++++++++++++ 6 files changed, 168 insertions(+), 46 deletions(-) delete mode 100644 test/e2e/setup.spec.js create mode 100644 test/e2e/specs/example.spec.js create mode 100644 test/e2e/specs/setup-process.once.js diff --git a/config/playwright.config.js b/config/playwright.config.js index 94239d2dd..0e186f3b5 100644 --- a/config/playwright.config.js +++ b/config/playwright.config.js @@ -1,11 +1,11 @@ import { defineConfig, devices } from "@playwright/test"; const port = 30001; -const url = `http://localhost:${port}`; +export const url = `http://localhost:${port}`; export default defineConfig({ // Look for test files in the "tests" directory, relative to this configuration file. - testDir: "../test/e2e", + testDir: "../test/e2e/specs", outputDir: "../private/playwright-test-results", fullyParallel: false, locale: "en-US", @@ -40,9 +40,15 @@ export default defineConfig({ // Configure projects for major browsers. projects: [ { - name: "chromium", + name: "run-once setup", + testMatch: /setup-process\.once\.js/, use: { ...devices["Desktop Chrome"] }, }, + { + name: "specs", + use: { ...devices["Desktop Chrome"] }, + dependencies: [ "run-once setup" ], + }, /* { name: "firefox", diff --git a/server/server.js b/server/server.js index c0a1422a8..321857b51 100644 --- a/server/server.js +++ b/server/server.js @@ -246,6 +246,36 @@ let needSetup = false; log.debug("test", request.body); response.send("OK"); }); + + const fs = require("fs"); + + app.get("/_e2e/take-sqlite-snapshot", async (request, response) => { + await Database.close(); + try { + fs.cpSync(Database.sqlitePath, `${Database.sqlitePath}.e2e-snapshot`); + } catch (err) { + throw new Error("Unable to copy SQLite DB."); + } + await Database.connect(); + + response.send("Snapshot taken."); + }); + + app.get("/_e2e/restore-sqlite-snapshot", async (request, response) => { + if (!fs.existsSync(`${Database.sqlitePath}.e2e-snapshot`)) { + throw new Error("Snapshot doesn't exist."); + } + + await Database.close(); + try { + fs.cpSync(`${Database.sqlitePath}.e2e-snapshot`, Database.sqlitePath); + } catch (err) { + throw new Error("Unable to copy snapshot file."); + } + await Database.connect(); + + response.send("Snapshot restored."); + }); } // Robots.txt diff --git a/test/e2e/setup.spec.js b/test/e2e/setup.spec.js deleted file mode 100644 index 55dcb34e6..000000000 --- a/test/e2e/setup.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import { test } from "@playwright/test"; -import { login, screenshot } from "./util-test"; - -/* - * Setup - */ - -test("setup sqlite", async ({ page }, testInfo) => { - await page.goto("./"); - await page.getByText("SQLite").click(); - await page.getByRole("button", { name: "Next" }).click(); - await screenshot(testInfo, page); -}); - -test("setup admin", async ({ page }, testInfo) => { - await page.goto("./"); - await page.getByPlaceholder("Username").click(); - await page.getByPlaceholder("Username").fill("admin"); - await page.getByPlaceholder("Username").press("Tab"); - await page.getByPlaceholder("Password", { exact: true }).fill("admin123"); - await page.getByPlaceholder("Password", { exact: true }).press("Tab"); - await page.getByPlaceholder("Repeat Password").fill("admin123"); - await page.getByRole("button", { name: "Create" }).click(); - await screenshot(testInfo, page); -}); - -/* - * All other tests should be run after setup - */ - -test("login", async ({ page }, testInfo) => { - await page.goto("./dashboard"); - await login(page); - await screenshot(testInfo, page); -}); - -test("logout", async ({ page }, testInfo) => { - await page.goto("./dashboard"); - await login(page); - await page.getByText("A", { exact: true }).click(); - await page.getByRole("button", { name: "Log out" }).click(); - await screenshot(testInfo, page); -}); diff --git a/test/e2e/specs/example.spec.js b/test/e2e/specs/example.spec.js new file mode 100644 index 000000000..27c605ddc --- /dev/null +++ b/test/e2e/specs/example.spec.js @@ -0,0 +1,38 @@ +import { expect, test } from "@playwright/test"; +import { login, restoreSqliteSnapshot, screenshot } from "../util-test"; + +test.describe("Example Spec", () => { + + test.beforeEach(async ({ page }) => { + await restoreSqliteSnapshot(page); + }); + + test("dashboard", async ({ page }, testInfo) => { + await page.goto("./dashboard"); + await login(page); + await screenshot(testInfo, page); + }); + + test("change display timezone", async ({ page }, testInfo) => { + await page.goto("./settings/general"); + await login(page); + await page.getByLabel("Display Timezone").selectOption("Pacific/Fiji"); + await page.getByRole("button", { name: "Save" }).click(); + await screenshot(testInfo, page); + + await page.goto("./dashboard"); + await page.goto("./settings/general"); + await expect(page.getByLabel("Display Timezone")).toHaveValue("Pacific/Fiji"); + }); + + test("database is reset after previous test", async ({ page }, testInfo) => { + await page.goto("./settings/general"); + await login(page); + + const timezoneEl = page.getByLabel("Display Timezone"); + await expect(timezoneEl).toBeVisible(); + await expect(timezoneEl).toHaveValue("auto"); + await screenshot(testInfo, page); + }); + +}); diff --git a/test/e2e/specs/setup-process.once.js b/test/e2e/specs/setup-process.once.js new file mode 100644 index 000000000..0dde0cdc4 --- /dev/null +++ b/test/e2e/specs/setup-process.once.js @@ -0,0 +1,56 @@ +import { test } from "@playwright/test"; +import { getSqliteDatabaseExists, login, screenshot, takeSqliteSnapshot } from "../util-test"; + +test.describe("Uptime Kuma Setup", () => { + + test.skip(() => getSqliteDatabaseExists(), "Must only run once per session"); + + /* + * Setup + */ + + test("setup sqlite", async ({ page }, testInfo) => { + await page.goto("./"); + await page.getByText("SQLite").click(); + await page.getByRole("button", { name: "Next" }).click(); + await screenshot(testInfo, page); + await page.waitForURL("/setup"); // ensures the server is ready to continue to the next test + await screenshot(testInfo, page); + }); + + test("setup admin", async ({ page }, testInfo) => { + await page.goto("./"); + await page.getByPlaceholder("Username").click(); + await page.getByPlaceholder("Username").fill("admin"); + await page.getByPlaceholder("Username").press("Tab"); + await page.getByPlaceholder("Password", { exact: true }).fill("admin123"); + await page.getByPlaceholder("Password", { exact: true }).press("Tab"); + await page.getByPlaceholder("Repeat Password").fill("admin123"); + await page.getByRole("button", { name: "Create" }).click(); + await screenshot(testInfo, page); + }); + + /* + * All other tests should be run after setup + */ + + test("login", async ({ page }, testInfo) => { + await page.goto("./dashboard"); + await login(page); + await screenshot(testInfo, page); + }); + + test("logout", async ({ page }, testInfo) => { + await page.goto("./dashboard"); + await login(page); + await page.getByText("A", { exact: true }).click(); + await page.getByRole("button", { name: "Log out" }).click(); + await screenshot(testInfo, page); + }); + + test("take sqlite snapshot", async ({ page }, testInfo) => { + await takeSqliteSnapshot(page); + await screenshot(testInfo, page); + }); + +}); diff --git a/test/e2e/util-test.js b/test/e2e/util-test.js index 285e7b716..f6af3cbd2 100644 --- a/test/e2e/util-test.js +++ b/test/e2e/util-test.js @@ -1,3 +1,9 @@ +const fs = require("fs"); +const path = require("path"); +const serverUrl = require("../../config/playwright.config.js").url; + +const dbPath = "./../../data/playwright-test/kuma.db"; + /** * @param {TestInfo} testInfo Test info * @param {Page} page Page @@ -25,3 +31,32 @@ export async function login(page) { await page.getByRole("button", { name: "Log in" }).click(); await page.isVisible("text=Add New Monitor"); } + +/** + * Determines if the SQLite database has been created. This indicates setup has completed. + * @returns {boolean} True if exists + */ +export function getSqliteDatabaseExists() { + return fs.existsSync(path.resolve(__dirname, dbPath)); +} + +/** + * Makes a request to the server to take a snapshot of the SQLite database. + * @param {Page|null} page Page + * @returns {Promise} Promise of response from snapshot request. + */ +export async function takeSqliteSnapshot(page = null) { + if (page) { + return page.goto("./_e2e/take-sqlite-snapshot"); + } else { + return fetch(`${serverUrl}/_e2e/take-sqlite-snapshot`); + } +} + +/** + * Makes a request to the server to restore the snapshot of the SQLite database. + * @returns {Promise} Promise of response from restoration request. + */ +export async function restoreSqliteSnapshot() { + return fetch(`${serverUrl}/_e2e/restore-sqlite-snapshot`); +}