.cursor/skills/playwright/SKILL.md
Write and maintain Playwright end-to-end tests for the Onyx application. Use when creating new E2E tests, debugging test failures, adding test coverage, or when the user mentions Playwright, E2E tests, or browser testing.
npx skillsauth add onyx-dot-app/onyx-foss playwright-e2e-testsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
web/tests/e2e/ — organized by feature (auth/, admin/, chat/, assistants/, connectors/, mcp/)web/playwright.config.tsweb/tests/e2e/utils/web/tests/e2e/constants.tsweb/tests/e2e/global-setup.tsweb/output/playwright/Always use absolute imports with the @tests/e2e/ prefix — never relative paths (../, ../../). The alias is defined in web/tsconfig.json and resolves to web/tests/.
import { loginAs } from "@tests/e2e/utils/auth";
import { OnyxApiClient } from "@tests/e2e/utils/onyxApiClient";
import { TEST_ADMIN_CREDENTIALS } from "@tests/e2e/constants";
All new files should be .ts, not .js.
# Run a specific test file
npx playwright test web/tests/e2e/chat/default_assistant.spec.ts
# Run a specific project
npx playwright test --project admin
npx playwright test --project exclusive
| Project | Description | Parallelism |
|---------|-------------|-------------|
| admin | Standard tests (excludes @exclusive) | Parallel |
| exclusive | Serial, slower tests (tagged @exclusive) | 1 worker |
All tests use admin_auth.json storage state by default (pre-authenticated admin session).
Global setup (global-setup.ts) runs automatically before all tests and handles:
[email protected] through [email protected]) (idempotent)admin_auth.json, admin2_auth.json, and worker{N}_auth.json for each worker user"worker" for each worker userBoth test projects set storageState: "admin_auth.json", so every test starts pre-authenticated as admin with no login code needed.
When a test needs a different user, use API-based login — never drive the login UI:
import { loginAs } from "@tests/e2e/utils/auth";
await page.context().clearCookies();
await loginAs(page, "admin2");
// Log in as the worker-specific user (preferred for test isolation):
import { loginAsWorkerUser } from "@tests/e2e/utils/auth";
await page.context().clearCookies();
await loginAsWorkerUser(page, testInfo.workerIndex);
Tests start pre-authenticated as admin — navigate and test directly:
import { test, expect } from "@playwright/test";
test.describe("Feature Name", () => {
test("should describe expected behavior clearly", async ({ page }) => {
await page.goto("/app");
await page.waitForLoadState("networkidle");
// Already authenticated as admin — go straight to testing
});
});
User isolation — tests that modify visible app state (creating assistants, sending chat messages, pinning items) should run as a worker-specific user and clean up resources in afterAll. Global setup provisions a pool of worker users ([email protected] through [email protected]). loginAsWorkerUser maps testInfo.workerIndex to a pool slot via modulo, so retry workers (which get incrementing indices beyond the pool size) safely reuse existing users. This ensures parallel workers never share user state, keeps usernames deterministic for screenshots, and avoids cross-contamination:
import { test } from "@playwright/test";
import { loginAsWorkerUser } from "@tests/e2e/utils/auth";
test.beforeEach(async ({ page }, testInfo) => {
await page.context().clearCookies();
await loginAsWorkerUser(page, testInfo.workerIndex);
});
If the test requires admin privileges and modifies visible state, use "admin2" instead — it's a pre-provisioned admin account that keeps the primary "admin" clean for other parallel tests. Switch to "admin" only for privileged setup (creating providers, configuring tools), then back to the worker user for the actual test. See chat/default_assistant.spec.ts for a full example.
loginAsRandomUser exists for the rare case where the test requires a brand-new user (e.g. onboarding flows). Avoid it elsewhere — it produces non-deterministic usernames that complicate screenshots.
API resource setup — only when tests need to create backend resources (image gen configs, web search providers, MCP servers). Use beforeAll/afterAll with OnyxApiClient to create and clean up. See chat/default_assistant.spec.ts or mcp/mcp_oauth_flow.spec.ts for examples. This is uncommon (~4 of 37 test files).
OnyxApiClient (@tests/e2e/utils/onyxApiClient)Backend API client for test setup/teardown. Key methods:
createFileConnector(), deleteCCPair(), pauseConnector()ensurePublicProvider(), createRestrictedProvider(), setProviderAsDefault()createAssistant(), deleteAssistant(), findAssistantByName()createUserGroup(), deleteUserGroup(), setUserRole()createWebSearchProvider(), createImageGenerationConfig()createChatSession(), deleteChatSession()chatActions (@tests/e2e/utils/chatActions)sendMessage(page, message) — sends a message and waits for AI responsestartNewChat(page) — clicks new-chat button and waits for introverifyDefaultAssistantIsChosen(page) — checks Onyx logo is visibleverifyAssistantIsChosen(page, name) — checks assistant name displayswitchModel(page, modelName) — switches LLM model via popovervisualRegression (@tests/e2e/utils/visualRegression)expectScreenshot(page, { name, mask?, hide?, fullPage? })expectElementScreenshot(locator, { name, mask?, hide? })VISUAL_REGRESSION=true env vartheme (@tests/e2e/utils/theme)THEMES — ["light", "dark"] as const array for iterating over both themessetThemeBeforeNavigation(page, theme) — sets next-themes theme via localStorage before navigationWhen tests need light/dark screenshots, loop over THEMES at the test.describe level and call setThemeBeforeNavigation in beforeEach before any page.goto(). Include the theme in screenshot names. See admin/admin_pages.spec.ts or chat/chat_message_rendering.spec.ts for examples:
import { THEMES, setThemeBeforeNavigation } from "@tests/e2e/utils/theme";
for (const theme of THEMES) {
test.describe(`Feature (${theme} mode)`, () => {
test.beforeEach(async ({ page }) => {
await setThemeBeforeNavigation(page, theme);
});
test("renders correctly", async ({ page }) => {
await page.goto("/app");
await expectScreenshot(page, { name: `feature-${theme}` });
});
});
}
tools (@tests/e2e/utils/tools)TOOL_IDS — centralized data-testid selectors for tool optionsopenActionManagement(page) — opens the tool management popoverUse locators in this priority order:
data-testid / aria-label — preferred for Onyx components
page.getByTestId("AppSidebar/new-session")
page.getByLabel("admin-page-title")
Role-based — for standard HTML elements
page.getByRole("button", { name: "Create" })
page.getByRole("dialog")
Text/Label — for visible text content
page.getByText("Custom Assistant")
page.getByLabel("Email")
CSS selectors — last resort, only when above won't work
page.locator('input[name="name"]')
page.locator("#onyx-chat-input-textarea")
Never use page.locator with complex CSS/XPath when a built-in locator works.
Use web-first assertions — they auto-retry until the condition is met:
// Visibility
await expect(page.getByTestId("onyx-logo")).toBeVisible({ timeout: 5000 });
// Text content
await expect(page.getByTestId("assistant-name-display")).toHaveText("My Assistant");
// Count
await expect(page.locator('[data-testid="onyx-ai-message"]')).toHaveCount(2, { timeout: 30000 });
// URL
await expect(page).toHaveURL(/chatId=/);
// Element state
await expect(toggle).toBeChecked();
await expect(button).toBeEnabled();
Never use assert statements or hardcoded page.waitForTimeout().
// Wait for load state after navigation
await page.goto("/app");
await page.waitForLoadState("networkidle");
// Wait for specific element
await page.getByTestId("chat-intro").waitFor({ state: "visible", timeout: 10000 });
// Wait for URL change
await page.waitForFunction(() => window.location.href.includes("chatId="), null, { timeout: 10000 });
// Wait for network response
await page.waitForResponse(resp => resp.url().includes("/api/chat") && resp.status() === 200);
"should display greeting message when opening new chat"OnyxApiClient for backend state; reserve UI interactions for the behavior under testloginAsWorkerUser(page, testInfo.workerIndex) (not admin) and clean up resources in afterAll. Each parallel worker gets its own user, preventing cross-contamination. Reserve loginAsRandomUser for flows that require a brand-new user (e.g. onboarding)utils/ with JSDoc commentswaitFor, waitForLoadState, or web-first assertions"E2E-CMD Chat 1") and clean up resources by ID in afterAll. This keeps screenshots deterministic and avoids needing to mask/hide dynamic text. Only fall back to timestamps (\test-${Date.now()}``) when resources cannot be reliably cleaned up or when name collisions across parallel workers would cause functional failures@exclusive in the test titleexpectScreenshot() for UI consistency checkstools
Query the Onyx knowledge base using the onyx-cli command. Use when the user wants to search company documents, ask questions about internal knowledge, query connected data sources, or look up information stored in Onyx.
documentation
Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions "deck," "slides," "presentation," or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill.
content-media
Generate images using nano banana.
tools
Query the Onyx knowledge base using the onyx-cli command. Use when the user wants to search company documents, ask questions about internal knowledge, query connected data sources, or look up information stored in Onyx.