Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions admin/playwright.dev.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineConfig } from "@playwright/test";

export default defineConfig({
fullyParallel: false,
workers: 1,
retries: 0,
reporter: "line",
timeout: 60_000,
expect: { timeout: 10_000 },
use: {
baseURL: "http://localhost:5050/admin/",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
webServer: {
command: "true",
port: 5050,
reuseExistingServer: true,
timeout: 5_000,
},
projects: [
{
name: "e2e",
testDir: "./tests/e2e",
testIgnore: [/setup\.spec\.ts$/, /_provisioning\.spec\.ts$/],
},
],
});
19 changes: 19 additions & 0 deletions admin/tests/e2e/account-pages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { test, expect } from "./fixtures";

test.describe("Account Settings page", () => {
test("loads and shows profile form", async ({ adminPage }) => {
await adminPage.goto("settings", { waitUntil: "networkidle" });
await expect(
adminPage.getByText(/account|profile|setting/i).first(),
).toBeVisible({ timeout: 10_000 });
});
});

test.describe("Appearance Settings page", () => {
test("loads and shows theme options", async ({ adminPage }) => {
await adminPage.goto("settings/appearance", { waitUntil: "networkidle" });
await expect(
adminPage.getByText(/appearance|theme|dark|light/i).first(),
).toBeVisible({ timeout: 10_000 });
});
});
37 changes: 37 additions & 0 deletions admin/tests/e2e/api-services-pages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { test, expect } from "./fixtures";

test.describe("API Explorer page", () => {
test("loads and shows API documentation", async ({ adminPage }) => {
await adminPage.goto("api/rest", { waitUntil: "networkidle" });
await expect(
adminPage.getByText(/api|endpoint|rest|swagger|openapi/i).first(),
).toBeVisible({ timeout: 10_000 });
});
});

test.describe("Realtime page", () => {
test("loads and shows realtime channels", async ({ adminPage }) => {
await adminPage.goto("realtime", { waitUntil: "networkidle" });
await expect(
adminPage.getByText(/realtime|channel|websocket|subscribe/i).first(),
).toBeVisible({ timeout: 10_000 });
});
});

test.describe("Email Settings page", () => {
test("loads and shows email provider config", async ({ adminPage }) => {
await adminPage.goto("email-settings", { waitUntil: "networkidle" });
await expect(
adminPage.getByText(/email|smtp|provider|sendgrid/i).first(),
).toBeVisible({ timeout: 10_000 });
});
});

test.describe("AI Providers page", () => {
test("loads and shows provider list", async ({ adminPage }) => {
await adminPage.goto("ai-providers", { waitUntil: "networkidle" });
await expect(
adminPage.getByText(/provider|openai|anthropic|ai/i).first(),
).toBeVisible({ timeout: 10_000 });
});
});
38 changes: 38 additions & 0 deletions admin/tests/e2e/auth-flows.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, expect } from "./fixtures";

test.describe("Auth flow pages", () => {
test("forgot password page renders", async ({ page }) => {
await page.goto("forgot-password", { waitUntil: "networkidle" });
await expect(page.locator("form")).toBeVisible();
await expect(page.getByText(/forgot.*password|reset.*password/i).first()).toBeVisible();
});

test("reset password page renders without token", async ({ page }) => {
await page.goto("reset-password", { waitUntil: "networkidle" });
const url = page.url();
expect(url).toContain("reset-password");
});

test("OTP login page renders", async ({ page }) => {
await page.goto("login/otp", { waitUntil: "networkidle" });
const url = page.url();
expect(url).toContain("login");
});

const errorPages = [
{ path: "401", status: 401, label: "Unauthorized" },
{ path: "403", status: 403, label: "Forbidden" },
{ path: "404", status: 404, label: "Not Found" },
{ path: "500", status: 500, label: "Internal Server Error" },
{ path: "503", status: 503, label: "Service Unavailable" },
];

for (const { path, status, label } of errorPages) {
test(`${status} error page renders`, async ({ adminPage }) => {
await adminPage.goto(path);
await expect(
adminPage.getByText(new RegExp(String(status))),
).toBeVisible({ timeout: 10_000 });
});
}
});
70 changes: 70 additions & 0 deletions admin/tests/e2e/database-pages.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { test, expect } from "./fixtures";

test.describe("Tables page", () => {
test("loads and shows table list or empty state", async ({ adminPage }) => {
await adminPage.goto("tables", { waitUntil: "networkidle" });

const apiErrors: string[] = [];
adminPage.on("response", (r) => {
if (r.status() >= 500) apiErrors.push(`${r.status()} ${r.url()}`);
});
expect(apiErrors).toEqual([]);

const hasTable = await adminPage.locator("table").isVisible().catch(() => false);
const hasEmpty = await adminPage.getByText(/no tables|empty|get started/i).isVisible().catch(() => false);
expect(hasTable || hasEmpty).toBeTruthy();
});
});

test.describe("Schema Viewer page", () => {
test("loads and renders schema tree", async ({ adminPage }) => {
await adminPage.goto("schema", { waitUntil: "networkidle" });

const apiErrors: string[] = [];
adminPage.on("response", (r) => {
if (r.status() >= 500) apiErrors.push(`${r.status()} ${r.url()}`);
});
expect(apiErrors).toEqual([]);

const hasTree = await adminPage.locator("[data-testid], [role='tree'], [role='treeitem']").first().isVisible().catch(() => false);
const hasContent = await adminPage.getByText(/schema|table|public/i).first().isVisible().catch(() => false);
expect(hasTree || hasContent).toBeTruthy();
});
});

test.describe("SQL Editor page", () => {
test("loads and shows editor area", async ({ adminPage }) => {
await adminPage.goto("sql-editor", { waitUntil: "networkidle" });

const apiErrors: string[] = [];
adminPage.on("response", (r) => {
if (r.status() >= 500) apiErrors.push(`${r.status()} ${r.url()}`);
});
expect(apiErrors).toEqual([]);

await expect(
adminPage.getByText(/sql|query|editor/i).first(),
).toBeVisible({ timeout: 10_000 });
});

test("can interact with SQL Editor page", async ({ adminPage }) => {
await adminPage.goto("sql-editor", { waitUntil: "networkidle" });

const editor = adminPage.locator("textarea, [contenteditable='true'], .cm-content, .monaco-editor textarea").first();
if (await editor.isVisible({ timeout: 5_000 }).catch(() => false)) {
await editor.click();
await adminPage.keyboard.type("SELECT 1");

const runButton = adminPage.getByRole("button", { name: /run|execute/i });
if (await runButton.isVisible({ timeout: 3_000 }).catch(() => false)) {
const [response] = await Promise.all([
adminPage.waitForResponse((r) => r.url().includes("/rpc") || r.url().includes("/sql"), { timeout: 10_000 }).catch(() => null),
runButton.click(),
]);
if (response) {
expect(response.status()).toBeLessThan(500);
}
}
}
});
});
36 changes: 36 additions & 0 deletions admin/tests/e2e/knowledge-base-detail.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { test, expect } from "./fixtures";
import { rawCreateKnowledgeBase } from "./helpers/api";

test.describe("Knowledge Base detail pages", () => {
test("detail sub-pages load for an existing knowledge base", async ({
adminPage,
adminToken,
}) => {
const kb = await rawCreateKnowledgeBase(
{ name: "E2E KB Detail Test", description: "Test KB for page smoke" },
adminToken,
);
const kbId = (kb.body as Record<string, unknown>).id as string;

const subPages = [
{ path: `knowledge-bases/${kbId}`, name: "Overview" },
{ path: `knowledge-bases/${kbId}/tables`, name: "Tables" },
{ path: `knowledge-bases/${kbId}/graph`, name: "Graph" },
{ path: `knowledge-bases/${kbId}/search`, name: "Search" },
{ path: `knowledge-bases/${kbId}/settings`, name: "Settings" },
];

for (const { path, name } of subPages) {
const apiErrors: string[] = [];
adminPage.on("response", (r) => {
if (r.status() >= 500) apiErrors.push(`${r.status()} ${r.url()}`);
});

await adminPage.goto(path, { waitUntil: "networkidle" });

expect(apiErrors, `${name}: no 500 errors`).toEqual([]);
const url = adminPage.url();
expect(url, `${name}: should stay on detail page`).toContain(kbId);
}
});
});
144 changes: 144 additions & 0 deletions admin/tests/e2e/page-smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { test, expect } from "./fixtures";

test.describe("Page smoke tests (instance admin)", () => {
const pages = [
{ path: "/", name: "Dashboard" },
{ path: "tables", name: "Tables" },
{ path: "schema", name: "Schema Viewer" },
{ path: "sql-editor", name: "SQL Editor" },
{ path: "users", name: "Users" },
{ path: "authentication", name: "Authentication" },
{ path: "knowledge-bases", name: "Knowledge Bases" },
{ path: "chatbots", name: "AI Chatbots" },
{ path: "mcp-tools", name: "MCP Tools" },
{ path: "api/rest", name: "API Explorer" },
{ path: "realtime", name: "Realtime" },
{ path: "storage", name: "Storage" },
{ path: "functions", name: "Functions" },
{ path: "jobs", name: "Jobs" },
{ path: "rpc", name: "RPC" },
{ path: "email-settings", name: "Email Settings" },
{ path: "ai-providers", name: "AI Providers" },
{ path: "policies", name: "RLS Policies" },
{ path: "security-settings", name: "Security Settings" },
{ path: "secrets", name: "Secrets" },
{ path: "client-keys", name: "Client Keys" },
{ path: "service-keys", name: "Service Keys" },
{ path: "webhooks", name: "Webhooks" },
{ path: "logs", name: "Log Stream" },
{ path: "monitoring", name: "Monitoring" },
{ path: "tenants", name: "Tenants" },
{ path: "features", name: "Configuration" },
{ path: "extensions", name: "Extensions" },
{ path: "database-config", name: "Database Config" },
{ path: "instance-settings", name: "Instance Settings" },
{ path: "storage-config", name: "Storage Config" },
{ path: "settings", name: "Account Settings" },
{ path: "settings/appearance", name: "Appearance" },
];

for (const { path, name } of pages) {
test(`${name} page loads without errors`, async ({ adminPage }) => {
const apiErrors: string[] = [];
const consoleErrors: string[] = [];

adminPage.on("response", (response) => {
if (response.status() >= 500) {
apiErrors.push(`${response.status()} ${response.url()}`);
}
});
adminPage.on("console", (msg) => {
if (msg.type() === "error") {
consoleErrors.push(msg.text());
}
});

await adminPage.goto(path, { waitUntil: "networkidle" });

expect(
apiErrors,
`${name}: no 500 API errors`,
).toEqual([]);
expect(
consoleErrors.filter((e) => !e.includes("favicon") && !e.includes("404")),
`${name}: no JS console errors`,
).toEqual([]);
});
}
});

test.describe("Page smoke tests (tenant admin)", () => {
const tenantAdminPages = [
{ path: "/", name: "Dashboard" },
{ path: "tables", name: "Tables" },
{ path: "schema", name: "Schema Viewer" },
{ path: "sql-editor", name: "SQL Editor" },
{ path: "users", name: "Users" },
{ path: "authentication", name: "Authentication" },
{ path: "knowledge-bases", name: "Knowledge Bases" },
{ path: "chatbots", name: "AI Chatbots" },
{ path: "mcp-tools", name: "MCP Tools" },
{ path: "api/rest", name: "API Explorer" },
{ path: "realtime", name: "Realtime" },
{ path: "storage", name: "Storage" },
{ path: "functions", name: "Functions" },
{ path: "jobs", name: "Jobs" },
{ path: "rpc", name: "RPC" },
{ path: "email-settings", name: "Email Settings" },
{ path: "ai-providers", name: "AI Providers" },
{ path: "policies", name: "RLS Policies" },
{ path: "security-settings", name: "Security Settings" },
{ path: "secrets", name: "Secrets" },
{ path: "client-keys", name: "Client Keys" },
{ path: "service-keys", name: "Service Keys" },
{ path: "webhooks", name: "Webhooks" },
{ path: "logs", name: "Log Stream" },
{ path: "monitoring", name: "Monitoring" },
{ path: "extensions", name: "Extensions" },
{ path: "settings", name: "Account Settings" },
{ path: "settings/appearance", name: "Appearance" },
];

const instanceOnlyPages = [
{ path: "tenants", name: "Tenants" },
{ path: "features", name: "Configuration" },
{ path: "database-config", name: "Database Config" },
{ path: "instance-settings", name: "Instance Settings" },
{ path: "storage-config", name: "Storage Config" },
];

for (const { path, name } of tenantAdminPages) {
test(`${name} page loads for tenant admin`, async ({
tenantAdminPage,
}) => {
const apiErrors: string[] = [];

tenantAdminPage.on("response", (response) => {
if (response.status() >= 500) {
apiErrors.push(`${response.status()} ${response.url()}`);
}
});

await tenantAdminPage.goto(path, { waitUntil: "networkidle" });

expect(
apiErrors,
`${name}: no 500 API errors for tenant admin`,
).toEqual([]);
});
}

for (const { path, name } of instanceOnlyPages) {
test(`${name} page is inaccessible for tenant admin`, async ({
tenantAdminPage,
}) => {
await tenantAdminPage.goto(path);
await tenantAdminPage.waitForLoadState("networkidle");
const url = tenantAdminPage.url();
expect(
url,
`${name}: tenant admin should be redirected away`,
).not.toMatch(new RegExp(path.replace("/", "\\/") + "(\\/|$|\\?)"));
});
}
});
Loading
Loading