feat(tests): add comprehensive Playwright E2E tests for navigation, shell, and smoke scenarios

This commit is contained in:
Ivan
2026-03-31 03:00:23 +03:00
parent 4ff4a12587
commit 0709ce4ba1
3 changed files with 246 additions and 0 deletions
+114
View File
@@ -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,
});
});
});
+81
View File
@@ -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");
});
});
+51
View File
@@ -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();
});
});