mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-28 21:26:10 +00:00
feat(tests): add comprehensive Playwright E2E tests for navigation, shell, and smoke scenarios
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const { PALETTE_PLACEHOLDER, openCommandPalette, prepareE2eSession } = require("./helpers");
|
||||
|
||||
test.describe("Getting started (tutorial page)", () => {
|
||||
test("tutorial route shows welcome copy", async ({ page }) => {
|
||||
await page.goto("/#/tutorial");
|
||||
await expect(page).toHaveURL(/#\/tutorial/);
|
||||
await expect(page.getByRole("heading", { name: /Welcome to\s*MeshChatX/i }).first()).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
await expect(
|
||||
page.getByText("The future of off-grid communication", { exact: false }).first(),
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Command palette", () => {
|
||||
test("Ctrl+K opens palette with search field", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
await openCommandPalette(page);
|
||||
await expect(page.getByPlaceholder(PALETTE_PLACEHOLDER)).toBeVisible();
|
||||
await expect(page.getByText("Navigate", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("palette navigates to Settings via filter and Enter", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
await openCommandPalette(page);
|
||||
const input = page.getByPlaceholder(PALETTE_PLACEHOLDER);
|
||||
await input.fill("Settings");
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(page).toHaveURL(/#\/settings/, { timeout: 15000 });
|
||||
await expect(page.getByText("Profile", { exact: true }).first()).toBeVisible({ timeout: 20000 });
|
||||
});
|
||||
|
||||
test("Escape closes command palette", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
await openCommandPalette(page);
|
||||
const input = page.getByPlaceholder(PALETTE_PLACEHOLDER);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByPlaceholder(PALETTE_PLACEHOLDER)).toBeHidden({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("palette Getting Started action opens tutorial modal", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
await openCommandPalette(page);
|
||||
const input = page.getByPlaceholder(PALETTE_PLACEHOLDER);
|
||||
await input.fill("Getting Started");
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(page.getByRole("heading", { name: /Welcome to\s*MeshChatX/i }).first()).toBeVisible({
|
||||
timeout: 20000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Sidebar and keyboard navigation", () => {
|
||||
test("sidebar links navigate across main sections", async ({ page, request }) => {
|
||||
test.setTimeout(120000);
|
||||
await prepareE2eSession(request);
|
||||
await page.goto("/#/messages");
|
||||
await expect(page).toHaveURL(/#\/messages/);
|
||||
|
||||
const sideNav = page.locator("ul.py-3");
|
||||
|
||||
await sideNav.locator('a[href*="#/nomadnetwork"]').click();
|
||||
await expect(page).toHaveURL(/#\/nomadnetwork/);
|
||||
|
||||
await sideNav.locator('a[href*="#/map"]').click();
|
||||
await expect(page).toHaveURL(/#\/map/);
|
||||
|
||||
await sideNav.locator('a[href*="#/tools"]').click();
|
||||
await expect(page).toHaveURL(/#\/tools/);
|
||||
await expect(page.getByText("Power tools for operators", { exact: true })).toBeVisible({
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
await sideNav.locator('a[href*="#/settings"]').click();
|
||||
await expect(page).toHaveURL(/#\/settings/);
|
||||
await expect(page.getByText("Profile", { exact: true }).first()).toBeVisible({ timeout: 20000 });
|
||||
|
||||
await sideNav.locator('a[href*="#/about"]').click();
|
||||
await expect(page).toHaveURL(/#\/about/);
|
||||
await expect(page.getByText("MeshChatX", { exact: true }).first()).toBeVisible({ timeout: 20000 });
|
||||
});
|
||||
|
||||
test("Alt+1 jumps to Messages from another route", async ({ page }) => {
|
||||
await page.goto("/#/contacts");
|
||||
await expect(page).toHaveURL(/#\/contacts/);
|
||||
await page.keyboard.press("Alt+1");
|
||||
await expect(page).toHaveURL(/#\/messages/, { timeout: 15000 });
|
||||
});
|
||||
|
||||
test("Alt+S opens Settings", async ({ page }) => {
|
||||
await page.goto("/#/map");
|
||||
await expect(page).toHaveURL(/#\/map/);
|
||||
await page.keyboard.press("Alt+s");
|
||||
await expect(page).toHaveURL(/#\/settings/, { timeout: 15000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Tools hub", () => {
|
||||
test("tools index lists utilities heading", async ({ page }) => {
|
||||
await page.goto("/#/tools");
|
||||
await expect(page.getByText("Utilities", { exact: true })).toBeVisible({ timeout: 20000 });
|
||||
await expect(page.getByPlaceholder("Search tools...", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("deep link to paper message tool", async ({ page }) => {
|
||||
await page.goto("/#/tools/paper-message");
|
||||
await expect(page).toHaveURL(/#\/tools\/paper-message/);
|
||||
await expect(page.getByRole("heading", { name: "Paper Message Generator", exact: true })).toBeVisible({
|
||||
timeout: 20000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const { prepareE2eSession } = require("./helpers");
|
||||
|
||||
function topChrome(page) {
|
||||
return page.locator("div.sticky.top-0.z-\\[100\\]").first();
|
||||
}
|
||||
|
||||
test.describe("Shell: sidebar, theme, notifications, call, search", () => {
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await prepareE2eSession(request);
|
||||
});
|
||||
|
||||
test("desktop sidebar collapse toggle changes width", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
const sidebar = page.locator("div.fixed.inset-y-0.left-0").filter({ has: page.locator("ul.py-3") });
|
||||
await expect(sidebar).toHaveClass(/w-80/);
|
||||
await page.locator("div.hidden.sm\\:flex.justify-end.p-2.border-b button").click();
|
||||
await expect(sidebar).toHaveClass(/w-16/);
|
||||
await page.locator("div.hidden.sm\\:flex.justify-end.p-2.border-b button").click();
|
||||
await expect(sidebar).toHaveClass(/w-80/);
|
||||
});
|
||||
|
||||
test("header theme button toggles dark mode on root shell", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
const shell = page.locator("#app > div.h-screen.w-full.flex.flex-col").first();
|
||||
await expect(shell).toBeVisible({ timeout: 30000 });
|
||||
const themeBtn = page.getByTitle(/Switch to (light|dark) mode/);
|
||||
await expect(themeBtn).toBeVisible({ timeout: 20000 });
|
||||
const initialDark = await shell.evaluate((el) => el.classList.contains("dark"));
|
||||
await themeBtn.click();
|
||||
await expect.poll(async () => shell.evaluate((el) => el.classList.contains("dark"))).not.toBe(initialDark);
|
||||
await themeBtn.click();
|
||||
await expect.poll(async () => shell.evaluate((el) => el.classList.contains("dark"))).toBe(initialDark);
|
||||
});
|
||||
|
||||
test("notification bell opens panel and closes from header", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
await topChrome(page)
|
||||
.locator("button")
|
||||
.filter({ has: page.locator('svg[aria-label="bell"]') })
|
||||
.click();
|
||||
await expect(page.getByRole("heading", { name: "Notifications", exact: true })).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
await expect(page.getByText("No new notifications", { exact: true })).toBeVisible({ timeout: 10000 });
|
||||
const panel = page.locator("div.fixed").filter({ hasText: "Notifications" }).first();
|
||||
await panel.locator('svg[aria-label="close"]').click();
|
||||
await expect(page.getByRole("heading", { name: "Notifications", exact: true })).toBeHidden({
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
test("call route shows Phone tab", async ({ page }) => {
|
||||
await page.goto("/#/call");
|
||||
await expect(page).toHaveURL(/#\/call/);
|
||||
await expect(page.getByRole("button", { name: "Phone", exact: true })).toBeVisible({ timeout: 20000 });
|
||||
});
|
||||
|
||||
test("messages Announces tab search input accepts text", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
await page.locator("div.-mb-px.flex").getByText("Announces", { exact: true }).click();
|
||||
const input = page.getByPlaceholder(/Search \d+ recent announces/);
|
||||
await expect(input).toBeVisible({ timeout: 15000 });
|
||||
await input.fill("e2e-filter");
|
||||
await expect(input).toHaveValue("e2e-filter");
|
||||
});
|
||||
|
||||
test("NomadNet favourites and announces search inputs accept text", async ({ page }) => {
|
||||
await page.goto("/#/nomadnetwork");
|
||||
const favSearch = page.getByPlaceholder(/Search \d+ favourites/);
|
||||
await expect(favSearch).toBeVisible({ timeout: 20000 });
|
||||
await favSearch.fill("fav-q");
|
||||
await expect(favSearch).toHaveValue("fav-q");
|
||||
|
||||
await page.locator("button.sidebar-tab").filter({ hasText: "Announces" }).click();
|
||||
const nodeSearch = page.getByPlaceholder(/Search \d+ recent announces/);
|
||||
await expect(nodeSearch).toBeVisible({ timeout: 15000 });
|
||||
await nodeSearch.fill("node-q");
|
||||
await expect(nodeSearch).toHaveValue("node-q");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
|
||||
const E2E_BACKEND_PORT = process.env.E2E_BACKEND_PORT || "8000";
|
||||
const E2E_BACKEND_ORIGIN = `http://127.0.0.1:${E2E_BACKEND_PORT}`;
|
||||
|
||||
test.describe("MeshChatX E2E (Vite + Python backend)", () => {
|
||||
test("backend /api/v1/status is OK (via Vite proxy)", async ({ request }) => {
|
||||
const res = await request.get("/api/v1/status");
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const body = await res.json();
|
||||
expect(body.status).toBe("ok");
|
||||
});
|
||||
|
||||
test("backend /api/v1/app/info returns version JSON (direct backend)", async ({ request }) => {
|
||||
const res = await request.get(`${E2E_BACKEND_ORIGIN}/api/v1/app/info`);
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const body = await res.json();
|
||||
expect(body.app_info).toBeDefined();
|
||||
expect(String(body.app_info.version).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("document title, shell, and app name in header", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveTitle(/Reticulum MeshChatX/);
|
||||
await expect(page.getByText("Reticulum MeshChatX", { exact: true }).first()).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
const root = page.locator("#app");
|
||||
await expect(root).toBeVisible();
|
||||
await expect(root.locator("div").first()).toBeVisible({ timeout: 30000 });
|
||||
});
|
||||
|
||||
test("about page shows MeshChatX and version", async ({ page }) => {
|
||||
await page.goto("/#/about");
|
||||
await expect(page).toHaveURL(/#\/about/);
|
||||
await expect(page.getByText("MeshChatX", { exact: true }).first()).toBeVisible({ timeout: 30000 });
|
||||
await expect(page.locator("#app")).toBeVisible();
|
||||
});
|
||||
|
||||
test("settings route loads profile section", async ({ page }) => {
|
||||
await page.goto("/#/settings");
|
||||
await expect(page).toHaveURL(/#\/settings/);
|
||||
await expect(page.getByText("Profile", { exact: true }).first()).toBeVisible({ timeout: 30000 });
|
||||
});
|
||||
|
||||
test("messages route is reachable", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
await expect(page).toHaveURL(/#\/messages/);
|
||||
await expect(page.locator("#app")).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user