diff options
Diffstat (limited to 'test/e2e')
-rw-r--r-- | test/e2e/specs/example.spec.js | 39 | ||||
-rw-r--r-- | test/e2e/specs/monitor-form.spec.js | 104 | ||||
-rw-r--r-- | test/e2e/specs/setup-process.once.js | 55 | ||||
-rw-r--r-- | test/e2e/specs/status-page.spec.js | 129 | ||||
-rw-r--r-- | test/e2e/util-test.js | 62 |
5 files changed, 389 insertions, 0 deletions
diff --git a/test/e2e/specs/example.spec.js b/test/e2e/specs/example.spec.js new file mode 100644 index 0000000..4fcfac6 --- /dev/null +++ b/test/e2e/specs/example.spec.js @@ -0,0 +1,39 @@ +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("set up monitor", async ({ page }, testInfo) => { + await page.goto("./add"); + await login(page); + + await expect(page.getByTestId("monitor-type-select")).toBeVisible(); + await page.getByTestId("monitor-type-select").selectOption("http"); + await page.getByTestId("friendly-name-input").fill("example.com"); + await page.getByTestId("url-input").fill("https://www.example.com/"); + await page.getByTestId("save-button").click(); + await page.waitForURL("/dashboard/*"); // wait for the monitor to be created + + await expect(page.getByTestId("monitor-list")).toContainText("example.com"); + await screenshot(testInfo, page); + }); + + test("database is reset after previous test", async ({ page }, testInfo) => { + await page.goto("./dashboard"); + await login(page); + + await expect(page.getByTestId("monitor-list")).not.toContainText("example.com"); + await screenshot(testInfo, page); + }); + +}); diff --git a/test/e2e/specs/monitor-form.spec.js b/test/e2e/specs/monitor-form.spec.js new file mode 100644 index 0000000..7a84f3c --- /dev/null +++ b/test/e2e/specs/monitor-form.spec.js @@ -0,0 +1,104 @@ +import { expect, test } from "@playwright/test"; +import { login, restoreSqliteSnapshot, screenshot } from "../util-test"; + +/** + * Selects the monitor type from the dropdown. + * @param {import('@playwright/test').Page} page - The Playwright page instance. + * @param {string} monitorType - The monitor type to select (default is "dns"). + * @returns {Promise<void>} - A promise that resolves when the monitor type is selected. + */ +async function selectMonitorType(page, monitorType = "dns") { + const monitorTypeSelect = page.getByTestId("monitor-type-select"); + await expect(monitorTypeSelect).toBeVisible(); + await monitorTypeSelect.selectOption(monitorType); + + const selectedValue = await monitorTypeSelect.evaluate((select) => select.value); + expect(selectedValue).toBe(monitorType); +} + +test.describe("Monitor Form", () => { + test.beforeEach(async ({ page }) => { + await restoreSqliteSnapshot(page); + }); + + test("condition ui", async ({ page }, testInfo) => { + await page.goto("./add"); + await login(page); + await screenshot(testInfo, page); + await selectMonitorType(page); + + await page.getByTestId("add-condition-button").click(); + expect(await page.getByTestId("condition").count()).toEqual(2); // 1 added by default + 1 explicitly added + + await page.getByTestId("add-group-button").click(); + expect(await page.getByTestId("condition-group").count()).toEqual(1); + expect(await page.getByTestId("condition").count()).toEqual(3); // 2 solo conditions + 1 condition in group + + await screenshot(testInfo, page); + + await page.getByTestId("remove-condition").first().click(); + expect(await page.getByTestId("condition").count()).toEqual(2); // 1 solo condition + 1 condition in group + + await page.getByTestId("remove-condition-group").first().click(); + expect(await page.getByTestId("condition-group").count()).toEqual(0); + + await screenshot(testInfo, page); + }); + + test("successful condition", async ({ page }, testInfo) => { + await page.goto("./add"); + await login(page); + await screenshot(testInfo, page); + await selectMonitorType(page); + + const friendlyName = "Example DNS NS"; + await page.getByTestId("friendly-name-input").fill(friendlyName); + await page.getByTestId("hostname-input").fill("example.com"); + + const resolveTypeSelect = page.getByTestId("resolve-type-select"); + await resolveTypeSelect.click(); + await resolveTypeSelect.getByRole("option", { name: "NS" }).click(); + + await page.getByTestId("add-condition-button").click(); + expect(await page.getByTestId("condition").count()).toEqual(2); // 1 added by default + 1 explicitly added + + await page.getByTestId("condition-value").nth(0).fill("a.iana-servers.net"); + await page.getByTestId("condition-and-or").nth(0).selectOption("or"); + await page.getByTestId("condition-value").nth(1).fill("b.iana-servers.net"); + + await screenshot(testInfo, page); + await page.getByTestId("save-button").click(); + await page.waitForURL("/dashboard/*"); + + expect(page.getByTestId("monitor-status")).toHaveText("up", { ignoreCase: true }); + + await screenshot(testInfo, page); + }); + + test("failing condition", async ({ page }, testInfo) => { + await page.goto("./add"); + await login(page); + await screenshot(testInfo, page); + await selectMonitorType(page); + + const friendlyName = "Example DNS NS"; + await page.getByTestId("friendly-name-input").fill(friendlyName); + await page.getByTestId("hostname-input").fill("example.com"); + + const resolveTypeSelect = page.getByTestId("resolve-type-select"); + await resolveTypeSelect.click(); + await resolveTypeSelect.getByRole("option", { name: "NS" }).click(); + + expect(await page.getByTestId("condition").count()).toEqual(1); // 1 added by default + + await page.getByTestId("condition-value").nth(0).fill("definitely-not.net"); + + await screenshot(testInfo, page); + await page.getByTestId("save-button").click(); + await page.waitForURL("/dashboard/*"); + + expect(page.getByTestId("monitor-status")).toHaveText("down", { ignoreCase: true }); + + 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 0000000..f1bdf2b --- /dev/null +++ b/test/e2e/specs/setup-process.once.js @@ -0,0 +1,55 @@ +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"); + + test.afterEach(async ({ page }, testInfo) => { + await screenshot(testInfo, page); + }); + + /* + * 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 + }); + + test("setup admin", async ({ page }) => { + 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(); + }); + + /* + * All other tests should be run after setup + */ + + test("login", async ({ page }) => { + await page.goto("./dashboard"); + await login(page); + }); + + test("logout", async ({ page }) => { + await page.goto("./dashboard"); + await login(page); + await page.getByText("A", { exact: true }).click(); + await page.getByRole("button", { name: "Log out" }).click(); + }); + + test("take sqlite snapshot", async ({ page }) => { + await takeSqliteSnapshot(page); + }); + +}); diff --git a/test/e2e/specs/status-page.spec.js b/test/e2e/specs/status-page.spec.js new file mode 100644 index 0000000..f525dfc --- /dev/null +++ b/test/e2e/specs/status-page.spec.js @@ -0,0 +1,129 @@ +import { expect, test } from "@playwright/test"; +import { login, restoreSqliteSnapshot, screenshot } from "../util-test"; + +test.describe("Status Page", () => { + + test.beforeEach(async ({ page }) => { + await restoreSqliteSnapshot(page); + }); + + test("create and edit", async ({ page }, testInfo) => { + // Monitor + const monitorName = "Monitor for Status Page"; + const tagName = "Client"; + const tagValue = "Acme Inc"; + + // Status Page + const footerText = "This is footer text."; + const refreshInterval = 30; + const theme = "dark"; + const googleAnalyticsId = "G-123"; + const customCss = "body { background: rgb(0, 128, 128) !important; }"; + const descriptionText = "This is an example status page."; + const incidentTitle = "Example Outage Incident"; + const incidentContent = "Sample incident message."; + const groupName = "Example Group 1"; + + // Set up a monitor that can be added to the Status Page + await page.goto("./add"); + await login(page); + await expect(page.getByTestId("monitor-type-select")).toBeVisible(); + await page.getByTestId("monitor-type-select").selectOption("http"); + await page.getByTestId("friendly-name-input").fill(monitorName); + await page.getByTestId("url-input").fill("https://www.example.com/"); + await page.getByTestId("add-tag-button").click(); + await page.getByTestId("tag-name-input").fill(tagName); + await page.getByTestId("tag-value-input").fill(tagValue); + await page.getByTestId("tag-color-select").click(); // Vue-Multiselect component + await page.getByTestId("tag-color-select").getByRole("option", { name: "Orange" }).click(); + await page.getByTestId("tag-submit-button").click(); + await page.getByTestId("save-button").click(); + await page.waitForURL("/dashboard/*"); // wait for the monitor to be created + + // Create a new status page + await page.goto("./add-status-page"); + await screenshot(testInfo, page); + + await page.getByTestId("name-input").fill("Example"); + await page.getByTestId("slug-input").fill("example"); + await page.getByTestId("submit-button").click(); + await page.waitForURL("/status/example?edit"); // wait for the page to be created + + // Fill in some details + await page.getByTestId("description-input").fill(descriptionText); + await page.getByTestId("footer-text-input").fill(footerText); + await page.getByTestId("refresh-interval-input").fill(String(refreshInterval)); + await page.getByTestId("theme-select").selectOption(theme); + await page.getByTestId("show-tags-checkbox").uncheck(); + await page.getByTestId("show-powered-by-checkbox").uncheck(); + await page.getByTestId("show-certificate-expiry-checkbox").uncheck(); + await page.getByTestId("google-analytics-input").fill(googleAnalyticsId); + await page.getByTestId("custom-css-input").getByTestId("textarea").fill(customCss); // Prism + await expect(page.getByTestId("description-editable")).toHaveText(descriptionText); + await expect(page.getByTestId("custom-footer-editable")).toHaveText(footerText); + + // Add an incident + await page.getByTestId("create-incident-button").click(); + await page.getByTestId("incident-title").isEditable(); + await page.getByTestId("incident-title").fill(incidentTitle); + await page.getByTestId("incident-content-editable").fill(incidentContent); + await page.getByTestId("post-incident-button").click(); + + // Add a group + await page.getByTestId("add-group-button").click(); + await page.getByTestId("group-name").isEditable(); + await page.getByTestId("group-name").fill(groupName); + + // Add the monitor + await page.getByTestId("monitor-select").click(); // Vue-Multiselect component + await page.getByTestId("monitor-select").getByRole("option", { name: monitorName }).click(); + await expect(page.getByTestId("monitor")).toHaveCount(1); + await expect(page.getByTestId("monitor-name")).toContainText(monitorName); + + // Save the changes + await screenshot(testInfo, page); + await page.getByTestId("save-button").click(); + await expect(page.getByTestId("edit-sidebar")).toHaveCount(0); + + // Ensure changes are visible + await expect(page.getByTestId("incident")).toHaveCount(1); + await expect(page.getByTestId("incident-title")).toContainText(incidentTitle); + await expect(page.getByTestId("incident-content")).toContainText(incidentContent); + await expect(page.getByTestId("description")).toContainText(descriptionText); + await expect(page.getByTestId("group-name")).toContainText(groupName); + await expect(page.getByTestId("footer-text")).toContainText(footerText); + await expect(page.getByTestId("powered-by")).toHaveCount(0); + + await expect(page.getByTestId("update-countdown-text")).toContainText("00:"); + const updateCountdown = Number((await page.getByTestId("update-countdown-text").textContent()).match(/(\d+):(\d+)/)[2]); + expect(updateCountdown).toBeGreaterThanOrEqual(refreshInterval); // cant be certain when the timer will start, so ensure it's within expected range + expect(updateCountdown).toBeLessThanOrEqual(refreshInterval + 10); + + await expect(page.locator("body")).toHaveClass(theme); + expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId); + + const backgroundColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor); + expect(backgroundColor).toEqual("rgb(0, 128, 128)"); + + await screenshot(testInfo, page); + + // Flip the "Show Tags" and "Show Powered By" switches: + await page.getByTestId("edit-button").click(); + await expect(page.getByTestId("edit-sidebar")).toHaveCount(1); + await page.getByTestId("show-tags-checkbox").setChecked(true); + await page.getByTestId("show-powered-by-checkbox").setChecked(true); + + await screenshot(testInfo, page); + await page.getByTestId("save-button").click(); + + await expect(page.getByTestId("edit-sidebar")).toHaveCount(0); + await expect(page.getByTestId("powered-by")).toContainText("Powered by"); + await expect(page.getByTestId("monitor-tag")).toContainText(tagValue); + + await screenshot(testInfo, page); + }); + + // @todo Test certificate expiry + // @todo Test domain names + +}); diff --git a/test/e2e/util-test.js b/test/e2e/util-test.js new file mode 100644 index 0000000..f6af3cb --- /dev/null +++ b/test/e2e/util-test.js @@ -0,0 +1,62 @@ +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 + * @returns {Promise<void>} + */ +export async function screenshot(testInfo, page) { + const screenshot = await page.screenshot(); + await testInfo.attach("screenshot", { + body: screenshot, + contentType: "image/png" + }); +} + +/** + * @param {Page} page Page + * @returns {Promise<void>} + */ +export async function login(page) { + // Login + await page.getByPlaceholder("Username").click(); + await page.getByPlaceholder("Username").fill("admin"); + await page.getByPlaceholder("Username").press("Tab"); + await page.getByPlaceholder("Password").fill("admin123"); + await page.getByLabel("Remember me").check(); + 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<Response>} 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<Response>} Promise of response from restoration request. + */ +export async function restoreSqliteSnapshot() { + return fetch(`${serverUrl}/_e2e/restore-sqlite-snapshot`); +} |