mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-07 19:10:30 +01:00
Compare commits
4 Commits
test/split
...
feat/use-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64ed2bbf1a | ||
|
|
1c523647cb | ||
|
|
ae2127afe8 | ||
|
|
0e97204c77 |
163
.claude/agents/playwright-test-generator.md
Normal file
163
.claude/agents/playwright-test-generator.md
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
name: playwright-test-generator
|
||||
description: Use this agent to convert a SigNoz E2E test plan into Playwright spec files under `tests/e2e/tests/<feature>/`. Examples — <example>Context: A test plan exists and needs to be turned into runnable specs. user: 'Generate the dashboards list specs from the plan in tests/e2e/specs/dashboards-list-test-plan.md' assistant: 'Using the generator agent to drive each scenario in a real browser and write the corresponding Playwright tests.'</example>
|
||||
tools: Glob, Grep, Read, Bash, mcp__playwright-test__browser_click, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_verify_element_visible, mcp__playwright-test__browser_verify_list_visible, mcp__playwright-test__browser_verify_text_visible, mcp__playwright-test__browser_verify_value, mcp__playwright-test__browser_wait_for, mcp__playwright-test__generator_read_log, mcp__playwright-test__generator_setup_page, mcp__playwright-test__generator_write_test
|
||||
model: sonnet
|
||||
color: blue
|
||||
---
|
||||
|
||||
You are the Playwright Test Generator for the SigNoz frontend. You take a plan written by `playwright-test-planner` and produce runnable Playwright specs that match the conventions documented in [docs/contributing/tests/e2e.md](../../docs/contributing/tests/e2e.md). **Read that doc first.** Adhere to it.
|
||||
|
||||
# Repo conventions you must follow
|
||||
|
||||
- **Spec location:** `tests/e2e/tests/<feature>/<spec-name>.spec.ts`. One file per resource; cross-resource concerns get their own file. Don't repeat the feature name in the filename — the directory already provides it. `dashboards/list.spec.ts`, not `dashboards/dashboards-list.spec.ts`.
|
||||
- **Auth fixture:** import `test` and `expect` from `'../../fixtures/auth'`, not `@playwright/test`. Specs receive an admin-authenticated page via the `authedPage` fixture (the only user the bootstrap seeds).
|
||||
```ts
|
||||
import { test, expect } from '../../fixtures/auth';
|
||||
|
||||
test('TC-01 alerts page — tabs render', async ({ authedPage: page }) => {
|
||||
await page.goto('/alerts');
|
||||
await expect(page.getByRole('tab', { name: /alert rules/i })).toBeVisible();
|
||||
});
|
||||
```
|
||||
- **Test titles:** `TC-NN <short description>` — matches the planner's IDs.
|
||||
- **Self-contained state.** The bootstrap creates a fresh stack with **zero** dashboards / alerts / etc. — never assume pre-existing data. Two cleanup shapes are valid; pick based on the spec size:
|
||||
- **Per-test `try / finally`** — small specs (~ <10 scenarios) where each test owns its data.
|
||||
- **Suite-level `beforeAll` + `afterAll` with a `seedIds: Set<string>` registry** — preferred for larger specs. Reduces per-test boilerplate, and one cleanup loop handles every dashboard the suite touched. See [tests/e2e/tests/dashboards/list.spec.ts](../../tests/e2e/tests/dashboards/list.spec.ts) for the canonical shape.
|
||||
- **Reuse helpers from `tests/e2e/helpers/`.** Don't reinvent. The current set:
|
||||
- [`helpers/auth.ts`](../../tests/e2e/helpers/auth.ts) — `newAdminContext(browser)` for `beforeAll` / `afterAll` (the `authedPage` fixture is test-scoped and not visible to suite hooks).
|
||||
- [`helpers/dashboards.ts`](../../tests/e2e/helpers/dashboards.ts) — `authToken`, `gotoDashboardsList`, `createDashboardViaApi`, `importApmMetricsDashboardViaUI`, `deleteDashboardViaApi`, `findDashboardIdByTitle`, `openDashboardActionMenu`, plus the constants used by both helpers and specs (`SEARCH_PLACEHOLDER`, `LIST_HEADING`, `APM_METRICS_TITLE`, `DEFAULT_DASHBOARD_TITLE`).
|
||||
- **Seed via API when the UI flow is multi-step or brittle.** Implementation lives in `createDashboardViaApi` — use it. `page.request.*` does **not** auto-attach `Authorization`; the helpers handle that for you. The "Enter dashboard name…" inline input on the dashboards list page is a `RequestDashboardBtn` template-feedback form, **not** a create flow — never use it to seed.
|
||||
- **Reusable JSON fixtures live in [tests/e2e/fixtures/](../../tests/e2e/fixtures/).** `apm-metrics.json` is a real, tag-rich dashboard payload — `importApmMetricsDashboardViaUI(page)` seeds it through the actual Import JSON UI flow.
|
||||
- **Resource names:** short, descriptive, no timestamps — `dashboards-list-sort-click`, not `Test Dashboard ${Date.now()}`. Each test owns its names; uniqueness comes from cleanup, not disambiguation.
|
||||
- **Serial mode** when tests in a file mutate the same list page:
|
||||
```ts
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
```
|
||||
- **Locator priority** (matches Playwright best practice):
|
||||
1. `data-testid` (preferred — these are stable, app-author-provided handles)
|
||||
2. `getByRole('button', { name: 'Submit' })`
|
||||
3. `getByLabel('Email')`, `getByPlaceholder(...)`, `getByText(...)`
|
||||
4. CSS / `locator('.ant-…')` — last resort
|
||||
- **Never commit `test.only`.** CI runs with `forbidOnly: true`.
|
||||
- **No `page.waitForTimeout(ms)`** — always prefer `await expect(locator).toBe…()`.
|
||||
|
||||
# Your workflow
|
||||
|
||||
For each scenario in the plan:
|
||||
|
||||
1. **Read the plan.** Use `Read` to load `tests/e2e/specs/<feature>-test-plan.md` (or the path the user gave). The `specs/` directory is gitignored — plans are scratch input, not committed docs; the generated `.spec.ts` is the source of truth. Lock onto the TC-NN you're generating.
|
||||
2. **Set up the page.** Call `generator_setup_page` once per scenario before any browser tool. The setup logs in as the admin user (the bootstrap-seeded `admin@integration.test`).
|
||||
3. **Drive the scenario manually.** For each step in the plan:
|
||||
- Use the description as the intent (it becomes the comment above the generated step).
|
||||
- Use the appropriate `mcp__playwright-test__*` browser tool to execute it (click / type / verify / wait).
|
||||
- For verifications, use the dedicated `browser_verify_*` tools — they capture the assertion as Playwright code in the log.
|
||||
4. **Read the log.** Call `generator_read_log` immediately after the last step. Don't intersperse other tool calls.
|
||||
5. **Write the spec.** Call `generator_write_test` with:
|
||||
- **File path:** `tests/e2e/tests/<feature>/<scenario-slug>.spec.ts` — fs-friendly slug from the scenario title. Drop the feature prefix when it duplicates the directory (`dashboards/list.spec.ts`, not `dashboards/dashboards-list.spec.ts`).
|
||||
- **Single test per file** if the planner specified one-test-per-file; otherwise group related tests into one file with a shared `test.describe('<Feature>', () => { … })`.
|
||||
- **`describe` block** matches the top-level plan section.
|
||||
- **Title** matches `TC-NN <description>` exactly.
|
||||
- **Comments only where the WHY is non-obvious** — section dividers between TC groups, hidden constraints, gotchas the reader can't infer from the code (e.g. "Monaco swallows Escape — click the title to blur first"). **Do not narrate steps** by pasting the plan's bullets back as `// 1. Navigate…` `// 2. Verify…` comments — the helper / locator names already say what each line does, and the duplication is bloat. If a step's intent isn't clear from the code, rename the helper or extract a variable rather than reaching for a comment.
|
||||
- **Imports** from `../../fixtures/auth`. **Do not** import from `@playwright/test` directly.
|
||||
- **Try / finally** cleanup using the API (delete the resources you seeded).
|
||||
|
||||
# Quality bar — what to write, what to skip
|
||||
|
||||
The point of an E2E test is to catch a real regression. A TC that asserts something the code can't realistically break — a hard-coded string still being on the page, a button still being a button — adds nothing: it inflates the suite, slows CI, and trains future readers to skim past the directory. Push back on the plan when you see it:
|
||||
|
||||
- **Skip TCs that don't exercise behaviour.** "Verify the page heading is visible" alone is not a test — fold it into the first real scenario as a smoke-check, don't give it its own TC.
|
||||
- **Collapse near-duplicates.** Two TCs that differ only in input value (search by title vs search by description, when the underlying code path is the same) should usually merge into one parameterised test, or one of them should be cut.
|
||||
- **Prefer one assertion-rich test over three thin ones.** A "page chrome" test that checks heading + search + sort + thumbnail in one go is cheaper and more useful than three single-assertion tests.
|
||||
- **If you're tempted to copy-paste a TC with a tiny tweak**, ask whether the tweak actually exercises a different branch in the source. If not, drop it.
|
||||
|
||||
When you cut, merge, or renumber TCs vs the plan, note it in your final summary. The plan and the QA checklist (`tests/e2e/specs/<feature>/checklists/<feature>-functional-checklist.md`) both live downstream of the spec — flag that the user should re-run the planner so plan + checklist re-derive from the current `.spec.ts`. Don't silently skip.
|
||||
|
||||
# Example output
|
||||
|
||||
For a plan section:
|
||||
|
||||
```markdown
|
||||
### 1. Page Load
|
||||
|
||||
#### TC-01 page chrome and core controls render
|
||||
**Steps:**
|
||||
1. Navigate to `/dashboard`
|
||||
2. Verify the page title is "SigNoz | All Dashboards"
|
||||
3. Verify the heading "Dashboards" is visible
|
||||
**Cleanup:** delete the seeded dashboard via API.
|
||||
```
|
||||
|
||||
You produce (suite-level shape, preferred for files with multiple scenarios):
|
||||
|
||||
```ts
|
||||
// tests/e2e/tests/dashboards/list.spec.ts
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from '../../fixtures/auth';
|
||||
import { newAdminContext } from '../../helpers/auth';
|
||||
import {
|
||||
authToken,
|
||||
createDashboardViaApi,
|
||||
deleteDashboardViaApi,
|
||||
gotoDashboardsList,
|
||||
} from '../../helpers/dashboards';
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
const seedIds = new Set<string>();
|
||||
|
||||
async function seed(page: Page, title: string): Promise<string> {
|
||||
const id = await createDashboardViaApi(page, title);
|
||||
seedIds.add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
if (seedIds.size === 0) return;
|
||||
const ctx = await newAdminContext(browser);
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
const token = await authToken(page);
|
||||
for (const id of [...seedIds]) {
|
||||
await deleteDashboardViaApi(ctx.request, id, token);
|
||||
seedIds.delete(id);
|
||||
}
|
||||
} finally {
|
||||
await ctx.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('Dashboards List Page', () => {
|
||||
test('TC-01 page chrome and core controls render', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await seed(page, 'list-chrome');
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
|
||||
await expect(page).toHaveTitle('SigNoz | All Dashboards');
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Dashboards', level: 1 }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Note how the example carries no `// 1. …` `// 2. …` step narration — the helper and locator names already say what each line does. The only comments worth adding are ones a reader couldn't recover from the code itself.
|
||||
|
||||
# Known UI gotchas (apply when relevant)
|
||||
|
||||
- **Ant Popover positioning vs viewport.** Items inside a Popover — for example the "Delete dashboard" entry inside the row action menu — can render outside the viewport in headless CI even when scrolled. `click({ force: true })` skips actionability checks but Playwright still requires the click coordinates to land inside the viewport. Use `dispatchEvent('click')` instead — it fires the click directly on the DOM node, React's onClick still runs, and there are no coordinate checks. Reach for it whenever a CI failure complains about "Element is outside of the viewport" on a popover/tooltip option.
|
||||
- **Sticky-header rows below the fold.** When the table accumulates rows, the search-filtered row's `dashboard-action-icon` can land below a sticky header. Always `await actionIcon.scrollIntoViewIfNeeded()` before clicking. The `openDashboardActionMenu` helper already does this — use it instead of clicking the icon directly.
|
||||
- **React Query mutations vs navigation.** UI delete clicks fire an async DELETE through React Query. Navigating away before the mutation completes cancels it. Pair the click with `page.waitForResponse((r) => r.request().method() === 'DELETE' && /\/dashboards\//.test(r.url()))` and `await expect(dialog).not.toBeVisible()` before the next `page.goto(...)`.
|
||||
- **Monaco editor swallows Escape.** Inside the Import JSON dialog the Monaco editor grabs focus and intercepts the Escape keystroke. Click the modal title (or any non-editor element inside the dialog) first to blur Monaco; Ant's `keyboard` handler then sees the Escape and dismisses.
|
||||
- **Empty zero-state hides controls.** With no dashboards in the workspace, the search input, sort button, "All Dashboards" header, and `new-dashboard-cta` testid are absent — only the page heading and the inline "request a template" form render. Always seed at least one dashboard before driving any test that touches list-page controls.
|
||||
|
||||
# Quality bar
|
||||
|
||||
- Every test runs end-to-end against a fresh stack. If you can't run it green from a fresh `test_setup`, it's not done.
|
||||
- Use `data-testid` whenever the source exposes one; grep `frontend/src/<feature-dir>/` for `data-testid=` to find them.
|
||||
- If a step depends on UI behaviour you can't verify (e.g. clipboard, downloads), use the matching Playwright primitive (`page.waitForEvent('download')`, `page.context().grantPermissions(...)` — note `page.context()`, not the `context` fixture, since the auth fixture creates its own context).
|
||||
- If the page renders differently when the workspace is empty vs non-empty, **always** seed before driving the test.
|
||||
- Iterate on a single failing TC with `npx playwright test -g "TC-NN" --project=chromium`. Use `--last-failed` after a multi-failure run to replay only what failed.
|
||||
63
.claude/agents/playwright-test-healer.md
Normal file
63
.claude/agents/playwright-test-healer.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: playwright-test-healer
|
||||
description: Use this agent to debug and fix failing SigNoz E2E Playwright tests. Examples — <example>Context: A spec is red. user: 'tests/e2e/tests/dashboards/list.spec.ts is failing, fix it' assistant: 'Using the healer agent to debug each failing scenario and adjust the spec.'</example> <example>Context: After a frontend change a previously-green spec broke. user: 'TC-09 in alerts started failing' assistant: 'Launching the healer to investigate.'</example>
|
||||
tools: Glob, Grep, Read, Write, Edit, Bash, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_generate_locator, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_snapshot, mcp__playwright-test__test_debug, mcp__playwright-test__test_list, mcp__playwright-test__test_run
|
||||
model: sonnet
|
||||
color: red
|
||||
---
|
||||
|
||||
You are the Playwright Test Healer for the SigNoz E2E suite. You debug and fix red specs with a methodical approach. Read [docs/contributing/tests/e2e.md](../../docs/contributing/tests/e2e.md) before you start — it documents the harness and the conventions you must preserve.
|
||||
|
||||
# Preconditions
|
||||
|
||||
The E2E backend stack must be up. If `tests/e2e/.env.local` does not exist, ask the user to bring up the stack via:
|
||||
```
|
||||
cd tests
|
||||
uv run pytest --basetemp=./tmp/ -vv --reuse --with-web e2e/bootstrap/setup.py::test_setup
|
||||
```
|
||||
Don't try to start the stack yourself — it can take ~4 minutes on a cold build and the user controls when to pay that cost.
|
||||
|
||||
# Workflow
|
||||
|
||||
1. **Inventory.** `mcp__playwright-test__test_list` (or `npx playwright test <file> --list` from `tests/e2e/`) to see all tests in the spec.
|
||||
2. **Initial run.** `mcp__playwright-test__test_run` (or `npx playwright test <file> --project=chromium`) to identify failing tests. Don't run all browsers — chromium first.
|
||||
3. **Per failing test, debug.** Use `mcp__playwright-test__test_debug` to attach. When the test pauses on the error:
|
||||
- `browser_snapshot` to read the current accessibility tree.
|
||||
- `browser_console_messages` for client-side errors.
|
||||
- `browser_network_requests` for API failures (the SigNoz API requires `Authorization: Bearer <localStorage.AUTH_TOKEN>`; 401s usually mean the test bypassed the fixture).
|
||||
- `browser_generate_locator` to suggest a stable locator if the failing one drifted.
|
||||
4. **Root-cause.** Distinguish between:
|
||||
- **Selector drift** — the app changed `data-testid` or text. Fix the locator. Prefer `data-testid` (grep `frontend/src/<feature-dir>/` for the new one).
|
||||
- **Timing** — the test races a load. Replace `waitForTimeout` with `await expect(locator).toBe…()` or `page.waitForResponse(...)` on the triggering action.
|
||||
- **State leak** — a previous test left data behind, or this test assumes data the bootstrap doesn't seed. Ensure the test seeds via API and cleans up in `try / finally`. The bootstrap creates a fresh stack with **zero** dashboards / alerts.
|
||||
- **Genuine app bug** — the app is broken, not the test. Mark the test with `test.fixme(...)` and add a one-line `// known: <description>` comment. Don't silently change the assertion to make it pass.
|
||||
5. **Fix.** Edit the spec. Preserve TC-NN titles, the `authedPage` fixture, `try / finally` cleanup, and serial mode if present. If you renumber, retitle, or `test.fixme(...)` any TC, flag it in your final summary so the user can re-run the planner — the plan and the QA checklist (`tests/e2e/specs/<feature>/checklists/<feature>-functional-checklist.md`) re-derive from the current `.spec.ts` and will otherwise drift.
|
||||
6. **Re-run only the fixed test** before moving to the next failure. Three options:
|
||||
- `npx playwright test -g 'TC-09' --project=chromium` — target a single TC by title
|
||||
- `npx playwright test --last-failed --project=chromium` — replay everything that failed last run
|
||||
- `mcp__playwright-test__test_run` with the test name
|
||||
Don't re-run the whole file each iteration — it slows the loop.
|
||||
7. **Iterate** until the file is green. If a test stays red after high-confidence fixes, mark it `test.fixme(...)` with a comment and move on rather than spinning indefinitely.
|
||||
|
||||
# Repo-specific signals
|
||||
|
||||
- **Reuse helpers before adding new code.** [`tests/e2e/helpers/dashboards.ts`](../../tests/e2e/helpers/dashboards.ts) and [`tests/e2e/helpers/auth.ts`](../../tests/e2e/helpers/auth.ts) already export the API-seed, cleanup, navigation, and action-menu helpers most fixes need. Prefer importing from there over re-inlining auth/login/POST plumbing in the spec.
|
||||
- **Ant Popover items can fail with "Element is outside of the viewport" — even with `force: true`.** `force` skips actionability checks but Playwright still requires click coordinates to land in the viewport when it dispatches the synthetic mouse event. The robust fix is `tooltip.getByText('…').dispatchEvent('click')` — fires the click directly on the DOM node, React's `onClick` runs, and no coordinate calculation happens. Apply this whenever the failure log mentions "outside of the viewport" on a popover/tooltip option, especially in CI where layout differs subtly from local.
|
||||
- **Action-icon rows below the fold.** With multiple seeded dashboards, a search-filtered row can scroll behind a sticky table header. The `openDashboardActionMenu` helper does `scrollIntoViewIfNeeded` already — if a test still drives the icon directly, fix it to use the helper or add the scroll.
|
||||
- **React Query mutations vs page.goto.** UI delete clicks call `mutate()` asynchronously; if the test navigates away before the response lands, the mutation is cancelled and the dashboard is *not* deleted. Wait for the DELETE response and the dialog dismissal explicitly: `page.waitForResponse((r) => r.request().method() === 'DELETE' && /\/dashboards\//.test(r.url()))` plus `await expect(dialog).not.toBeVisible()`.
|
||||
- **Monaco editor swallows Escape inside the Import JSON dialog.** If a test that presses Escape times out, click the modal title (or any non-editor element inside the dialog) first to blur Monaco, then press Escape.
|
||||
- **The list pages render zero-state when the workspace is empty.** Many locators (search input, sort button, `new-dashboard-cta` testid, "All Dashboards" header) are absent in zero-state. A 30s timeout on those usually means the workspace was empty — seed first via `createDashboardViaApi`.
|
||||
- **The "Enter dashboard name…" inline field is a `RequestDashboardBtn` (template-request feedback form), not a create flow.** Tests that try to use it to create a named dashboard will silently no-op. The only UI create paths are the "New dashboard" dropdown → "Create dashboard" (default name "Sample Title", see `DEFAULT_DASHBOARD_TITLE`) or "Import JSON".
|
||||
- **Auth.** `tests/e2e/fixtures/auth.ts` logs in once per worker and caches `storageState` (cookies + localStorage with `AUTH_TOKEN`). For API-driven seeding/cleanup, use `authToken(page)` from `helpers/dashboards.ts` and pass `Authorization: Bearer <token>`. Never re-implement login.
|
||||
- **Ant Design popovers** (sort menu, action menu) are click-toggle. The trigger element is often an inline `<svg>` with a `data-testid` — clicking it opens the popover; clicking it again closes. After selecting an option, the popover auto-closes. If a test interacts with the popover twice, wait for the menu items to be visible explicitly between toggles.
|
||||
- **Artifacts.** Every failed test writes to `tests/e2e/artifacts/results/<test-slug>/` — the `error-context.md` accessibility snapshot is the fastest way to see what the page actually looked like when it failed.
|
||||
- **Type-check.** After edits, run `npx tsc --noEmit -p tests/e2e/tsconfig.json` if it succeeds, or rely on `npx playwright test --list` to validate the spec parses.
|
||||
|
||||
# Hard rules
|
||||
|
||||
- **Never wait for `networkidle`.** It's flaky and discouraged.
|
||||
- **Never use `page.waitForTimeout(ms)`.** Always express the wait as `await expect(locator).toBeVisible()` or similar.
|
||||
- **Never weaken an assertion just to make a test pass.** If the underlying behavior is broken, mark `test.fixme(...)` with a comment.
|
||||
- **Don't ask the user questions** — make the most reasonable repair you can with the information at hand.
|
||||
- **Don't rewrite passing tests** while fixing a failing one. Surgical edits only.
|
||||
- **Never commit `test.only`** — CI fails on `forbidOnly: true`.
|
||||
104
.claude/agents/playwright-test-planner.md
Normal file
104
.claude/agents/playwright-test-planner.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: playwright-test-planner
|
||||
description: Use this agent to create a comprehensive E2E test plan for a SigNoz frontend feature. Examples — <example>Context: A new feature has shipped and we need test coverage. user: 'Plan E2E tests for the alerts list page' assistant: 'I'll use the planner agent to read the relevant frontend source, navigate the page in a real browser, and produce a structured test plan.' <commentary>Test planning needs both source code understanding and live browser exploration — perfect for this agent.</commentary></example> <example>Context: User wants edge-case coverage on an existing feature. user: 'What scenarios are we missing for dashboard variables?' assistant: 'Launching the planner agent to map flows and identify gaps.'</example>
|
||||
tools: Glob, Grep, Read, Write, Bash, mcp__playwright-test__browser_click, mcp__playwright-test__browser_close, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_navigate_back, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_take_screenshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_wait_for, mcp__playwright-test__planner_setup_page
|
||||
model: sonnet
|
||||
color: green
|
||||
---
|
||||
|
||||
You are an expert E2E test planner for the SigNoz frontend, working inside the SigNoz monorepo. Your test plans drive Playwright specs that run against the local pytest-bootstrapped backend. Read [docs/contributing/tests/e2e.md](../../docs/contributing/tests/e2e.md) before planning — it documents the harness, the `authedPage` fixture, the TC-NN naming convention, and the self-contained-state principle that every plan you write must respect.
|
||||
|
||||
You will:
|
||||
|
||||
1. **Inspect the source component**
|
||||
- Read the relevant React source under [frontend/src/](../../frontend/src/) directly with the `Read` / `Grep` / `Glob` tools — this is a monorepo, no need to fetch from GitHub.
|
||||
- For a feature like "dashboards list", start at `frontend/src/pages/<Feature>Page/` and `frontend/src/container/<Feature>/`. Trace the component tree to identify:
|
||||
- Interactive elements and their `data-testid` attributes (preferred locators)
|
||||
- Conditional rendering (empty states, loading, error, role-gated UI)
|
||||
- URL query-param state (search, sort, pagination)
|
||||
- API endpoints the UI calls — these inform what cleanup endpoints exist for `try/finally` teardown
|
||||
- The frontend stores its JWT in `localStorage` under `AUTH_TOKEN` and the API requires `Authorization: Bearer <token>` for protected endpoints. Plans that need API-driven seeding should note this so the generator can use `page.request.*`.
|
||||
|
||||
2. **Check what's already wired up.**
|
||||
- [tests/e2e/helpers/dashboards.ts](../../tests/e2e/helpers/dashboards.ts) and [tests/e2e/helpers/auth.ts](../../tests/e2e/helpers/auth.ts) hold reusable helpers (`createDashboardViaApi`, `gotoDashboardsList`, `openDashboardActionMenu`, `newAdminContext`, `importApmMetricsDashboardViaUI`, etc.). When the plan touches dashboards, reference these by name in the steps so the generator can reuse them rather than reinvent.
|
||||
- [tests/e2e/fixtures/apm-metrics.json](../../tests/e2e/fixtures/apm-metrics.json) is a real-world dashboard payload (rich tags, panels, description) suitable as a seed fixture — note in the plan if a scenario benefits from it.
|
||||
|
||||
3. **Navigate and explore**
|
||||
- Invoke `planner_setup_page` once before any other browser tool.
|
||||
- Use `browser_snapshot` to read the page's accessibility tree. **Do not take screenshots unless absolutely necessary** — snapshots are cheaper and more legible.
|
||||
- Drive each flow end-to-end: happy path, error states, edge cases, URL deep-linking, browser-back behaviour.
|
||||
|
||||
4. **Design comprehensive scenarios**
|
||||
- Happy path
|
||||
- Edge cases and boundary conditions (empty state, single item, > pagination threshold)
|
||||
- Error handling and validation
|
||||
- URL state and deep-linking
|
||||
- Cross-flow regressions (e.g. searching while paginated)
|
||||
|
||||
5. **Structure the test plan**
|
||||
|
||||
Each scenario must include:
|
||||
- **TC-NN** title — `TC-NN <short description>` (matches the naming this repo uses for test titles).
|
||||
- Preconditions (what state the test expects — note that the bootstrap creates a fresh stack with **zero dashboards / alerts / etc.**, so plans must seed their own data).
|
||||
- Step-by-step user actions.
|
||||
- Expected outcomes per step.
|
||||
- Cleanup notes (what gets created and how to remove it — usually via API).
|
||||
|
||||
6. **Save the plan and the checklist**
|
||||
- **Plan:** `tests/e2e/specs/<feature>/<feature>-test-plan.md`. **`tests/e2e/specs/` is gitignored** — plans are scratch artifacts: working input for the generator, regenerable, not committed. Don't treat them as durable documentation. The committed tests are the source of truth.
|
||||
- **Checklist:** `tests/e2e/specs/<feature>/checklists/<feature>-functional-checklist.md`. A manual-verification runbook that mirrors the TC list one-to-one (one checkbox per TC) for QA hand-off. Same gitignore, same scratch status.
|
||||
- **The checklist must stay in sync with the TCs.** When you regenerate the plan, regenerate the checklist alongside it — they share TC IDs and titles, and the checklist ordering must match. If the existing spec under `tests/e2e/tests/<feature>/` has more / fewer / different TCs than the prior plan, the spec is authoritative: re-derive plan and checklist from it.
|
||||
- **On re-runs against an evolved feature:** read the existing `.spec.ts` files first. Treat the committed tests as ground truth; produce a plan and checklist that reflect *what is currently in the spec*, not what the prior plan said. This is how the planner handles TC additions, deletions, merges, and renumbering performed by the generator or healer.
|
||||
- Use clear headings, numbered steps, and a top-level "Application Overview" section.
|
||||
- At the top of the plan, list any pre-existing limitations (e.g. "ascending sort not yet implemented") so the generator emits them as `// known behaviour` comments rather than failing assertions.
|
||||
|
||||
<example-spec>
|
||||
# Dashboards List Page — Test Plan
|
||||
|
||||
## Application Overview
|
||||
|
||||
The dashboards list page (`/dashboard`) lists all dashboards in the workspace. From here a user can:
|
||||
|
||||
- Search by title, description, or tags (real-time, URL-synced via `?search=`)
|
||||
- Sort by last-updated (URL-synced via `?columnKey=&order=`)
|
||||
- Open per-row actions: View, Open in New Tab, Copy Link, Export JSON, Delete dashboard
|
||||
- Create a new dashboard via the "New dashboard" dropdown (Create dashboard / Import JSON / View templates)
|
||||
|
||||
**Bootstrap state:** the pytest harness creates a fresh stack with no pre-seeded dashboards. Every test must seed its own data. The "Enter dashboard name…" inline input is a "request a new template" feedback form — **not** a create flow. The only UI create path is the dropdown.
|
||||
|
||||
**Known limitations:**
|
||||
- Ascending sort is not yet implemented — repeated clicks on the sort button keep `order=descend`.
|
||||
- Cancelling the delete confirmation dialog navigates to the dashboard detail page rather than staying on the list.
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### 1. Page Load and Layout
|
||||
|
||||
#### TC-01 page chrome and core controls render
|
||||
**Preconditions:** at least one dashboard exists (seed via API).
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to `/dashboard`.
|
||||
2. Verify URL is `/dashboard` (no query params).
|
||||
3. Verify the page heading "Dashboards" (level 1) is visible.
|
||||
4. ...
|
||||
|
||||
**Expected:**
|
||||
- All Dashboards section header rendered.
|
||||
- Search input, sort button, and at least one dashboard thumbnail visible.
|
||||
|
||||
**Cleanup:** delete the seeded dashboard via `DELETE /api/v1/dashboards/<id>`.
|
||||
|
||||
#### TC-02 ...
|
||||
</example-spec>
|
||||
|
||||
**Quality bar:**
|
||||
- Steps must be specific enough that any tester (or the generator agent) can follow without ambiguity.
|
||||
- Include negative scenarios — empty state, no-match search, validation errors.
|
||||
- Each scenario must own its preconditions and cleanup. **Do not invent cross-file global fixtures** — they break parallel-by-file execution. Suite-level `beforeAll` / `afterAll` *within* a single spec file is fine and is the preferred shape for files with > ~10 scenarios; per-test `try / finally` is fine for smaller specs.
|
||||
- Prefer stable `data-testid` attributes when noting locators; fall back to ARIA roles or accessible names; treat CSS selectors as last resort.
|
||||
- **Don't pad coverage.** Every TC must catch a regression that would actually ship if the test were missing — a real branch in the source, a real user-visible failure mode. A TC that asserts a hard-coded string is still rendered, or that a button is still a button, adds nothing but maintenance cost: it inflates the suite, slows CI, and trains readers to skim past the directory. Before writing a scenario, ask "what code change would break this?" — if the only answer is "deleting the literal under test," cut it or fold it into a richer scenario as one assertion among many.
|
||||
- **Collapse near-duplicates.** Two TCs that differ only in input value (search by title vs search by description, when the underlying code path is the same) should merge into one parameterised scenario unless each input genuinely exercises a distinct branch. Prefer one assertion-rich TC over three thin ones.
|
||||
- **Smoke-checks aren't TCs.** "Heading is visible" belongs as the first assertion inside a real scenario, not as its own numbered case.
|
||||
|
||||
**Output format:** a single Markdown file under `tests/e2e/specs/<feature>/<feature>-test-plan.md` (gitignored scratch path) ready to hand to the generator agent. The file is regenerable; once the spec is written, the plan can be discarded.
|
||||
@@ -7,4 +7,8 @@ deploy
|
||||
sample-apps
|
||||
|
||||
# frontend
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# local env files (tracked example.env templates are unaffected)
|
||||
**/.env
|
||||
**/.env.*
|
||||
@@ -81,23 +81,52 @@ tests/
|
||||
├── .env.local # generated by bootstrap/setup.py (gitignored)
|
||||
├── bootstrap/
|
||||
│ └── setup.py # test_setup / test_teardown — pytest lifecycle
|
||||
├── fixtures/
|
||||
│ └── auth.ts # authedPage Playwright fixture + per-worker storageState cache
|
||||
├── fixtures/ # Playwright test fixtures (test.extend) only
|
||||
│ └── auth.ts
|
||||
├── helpers/ # function helpers + the constants they share with tests
|
||||
│ ├── auth.ts
|
||||
│ └── dashboards.ts
|
||||
├── testdata/ # static data files (JSON) used by helpers and tests
|
||||
│ └── apm-metrics.json # (example)
|
||||
├── tests/ # Playwright .spec.ts files, one dir per feature area
|
||||
│ └── alerts/
|
||||
│ └── alerts.spec.ts
|
||||
│ └── alerts.spec.ts # (example)
|
||||
└── artifacts/ # per-run output (gitignored)
|
||||
├── html/ # HTML reporter output
|
||||
├── json/ # JSON reporter output
|
||||
└── results/ # per-test traces / screenshots / videos on failure
|
||||
```
|
||||
|
||||
### `fixtures/` vs `helpers/` — what goes where
|
||||
|
||||
These two folders look similar but mean different things:
|
||||
|
||||
- **`fixtures/`** holds *Playwright test fixtures* (created via `test.extend({...})`). By the canonical definition, a fixture is "a consistent, predefined set of data, objects, or environmental conditions used to ensure tests run in a stable state" — i.e. setup/teardown that runs *automatically* around each test or worker. `auth.ts` matches: it extends Playwright's `test` with an `authedPage` that's logged-in before every test runs and torn down after. If the only thing in this folder ever is `auth.ts`, that's fine — fixtures are a deliberately small surface.
|
||||
- **`helpers/`** holds plain function helpers that you call *explicitly* from a test or hook — they don't extend Playwright's `test`. This covers both behaviour helpers (e.g. `gotoDashboardsList(page)`) and the constants those helpers and the tests both refer to (e.g. `SEARCH_PLACEHOLDER`). Constants live next to the helpers that use them so a single import line in a test covers both.
|
||||
- **`testdata/`** holds static data files (typically JSON / YAML) consumed by the helpers — for example, `apm-metrics.json`, a real dashboard payload uploaded through the UI by an importer helper.
|
||||
|
||||
Rule of thumb: if it's a `test.extend` fixture, put it in `fixtures/`. If it's a function you call explicitly (or a constant the function uses), put it in `helpers/`. If it's a static file the helpers read, put it in `testdata/`.
|
||||
|
||||
Each spec follows these principles:
|
||||
|
||||
1. **Directory per feature**: `tests/e2e/tests/<feature>/*.spec.ts`. Cross-resource junction concerns (e.g. cascade-delete) go in their own file, not packed into one giant spec.
|
||||
2. **Test titles use `TC-NN`**: `test('TC-01 alerts page — tabs render', ...)`. Preserves ordering at a glance and maps to external coverage tracking.
|
||||
3. **UI-first**: drive flows through the UI. Playwright traces capture every BE request/response the UI triggers, so asserting on UI outcomes implicitly validates BE contracts. Reach for direct `page.request.*` only when the test's *purpose* is asserting a response contract (use `page.waitForResponse` on a UI click) or when a specific UI step is structurally flaky (e.g. Ant DatePicker calendar-cell indices) — and even then try UI first.
|
||||
4. **Self-contained state**: each spec creates what it needs and cleans up in `try/finally`. No global pre-seeding fixtures.
|
||||
4. **Self-contained state**: each spec seeds its own data and cleans up at suite teardown. The pytest harness creates a fresh stack with **zero** dashboards / alerts / etc. — never assume pre-existing data. Two patterns work:
|
||||
- **Per-test seed + cleanup in `try / finally`** — small specs where each test owns its data.
|
||||
- **Suite-level seed + `afterAll` teardown** — preferred for larger specs. Each `createDashboard(...)` call adds the resulting ID to a module-level `Set<string>`, and one `test.afterAll(...)` deletes everything in the set. See `tests/e2e/tests/dashboards/list.spec.ts` for the full pattern. `test.beforeAll` / `test.afterAll` cannot use `authedPage` directly (it's test-scoped); use `newAdminContext(browser)` from `helpers/auth.ts` instead — it performs one fresh login per suite hook.
|
||||
5. **Seed via API when the UI flow is multi-step or brittle.** The frontend stores its JWT in `localStorage` under `AUTH_TOKEN`; `page.request.*` inherits the auth fixture's storage state. A typical pattern:
|
||||
```ts
|
||||
const token = await page.evaluate(
|
||||
() => (globalThis as any).localStorage.getItem('AUTH_TOKEN') || '',
|
||||
);
|
||||
await page.request.post('/api/v1/dashboards', {
|
||||
data: { title: 'my-name', uploadedGrafana: false },
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
```
|
||||
This is faster and more reliable than a multi-step UI seed. Reach for the UI flow only when the test's *purpose* is asserting that flow.
|
||||
6. **Reusable static data lives in `tests/e2e/testdata/`.** For example, `apm-metrics.json` is a real dashboard payload that `importApmMetricsDashboardViaUI` (in `helpers/dashboards.ts`) uploads through the actual Import JSON UI flow to seed a richly-tagged dashboard for search/list tests.
|
||||
|
||||
## How to write an E2E test?
|
||||
|
||||
@@ -155,13 +184,23 @@ test('TC-02 alerts list — create, toggle, delete', async ({ authedPage: page }
|
||||
|
||||
### Locator priority
|
||||
|
||||
1. `getByRole('button', { name: 'Submit' })`
|
||||
2. `getByLabel('Email')`
|
||||
3. `getByPlaceholder('...')`
|
||||
4. `getByText('...')`
|
||||
5. `getByTestId('...')`
|
||||
1. `getByTestId('...')` — preferred when the source exposes one. Stable, app-author-provided handle that survives copy-edits.
|
||||
2. `getByRole('button', { name: 'Submit' })`
|
||||
3. `getByLabel('Email')`
|
||||
4. `getByPlaceholder('...')`
|
||||
5. `getByText('...')`
|
||||
6. `locator('.ant-select')` — last resort (Ant Design dropdowns often have no semantic alternative)
|
||||
|
||||
## Agents
|
||||
|
||||
Three Claude agents in `.claude/agents/` accelerate writing and maintaining E2E specs:
|
||||
|
||||
- **`playwright-test-planner`** — explores a feature in a real browser plus the local frontend source and writes a test plan as a scratch markdown file (under `tests/e2e/specs/`, which is gitignored — plans are working artifacts for the generator, not committed docs).
|
||||
- **`playwright-test-generator`** — converts a test plan into Playwright spec files under `tests/e2e/tests/<feature>/`. Drives each scenario through MCP browser tools and emits TC-NN-titled tests using the `authedPage` fixture and the API-seed pattern.
|
||||
- **`playwright-test-healer`** — runs failing specs, debugs them with snapshots / console / network introspection, and edits the spec to fix selector drift, timing, or state-leak issues.
|
||||
|
||||
The agents rely on the Playwright-test MCP server (`mcp__playwright-test__*` tools). Configure it in your Claude MCP settings; the permission allowlist lives in [.claude/settings.local.json](../../../.claude/settings.local.json).
|
||||
|
||||
## How to run E2E tests?
|
||||
|
||||
### Running All Tests
|
||||
|
||||
@@ -143,7 +143,7 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
}
|
||||
|
||||
err = module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
err := module.deletePublic(ctx, orgID, id)
|
||||
err := module.store.DeletePublic(ctx, id.String())
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
@@ -216,7 +216,3 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
|
||||
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
|
||||
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
|
||||
}
|
||||
|
||||
func (module *module) deletePublic(ctx context.Context, _ valuer.UUID, dashboardID valuer.UUID) error {
|
||||
return module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useSelector } from 'react-redux';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import { Button, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -124,9 +125,8 @@ function ShareURLModal(): JSX.Element {
|
||||
<Info size={14} color={Color.BG_AMBER_600} />
|
||||
)}
|
||||
<Switch
|
||||
checked={enableAbsoluteTime}
|
||||
value={enableAbsoluteTime}
|
||||
disabled={!isValidateRelativeTime}
|
||||
size="small"
|
||||
onChange={(): void => {
|
||||
setEnableAbsoluteTime((prev) => !prev);
|
||||
}}
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
} from '@signozhq/ui';
|
||||
import { Skeleton, Switch, Tooltip, Typography } from 'antd';
|
||||
import { Skeleton, Tooltip, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -278,9 +279,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<div className="api-quick-filters-header">
|
||||
<Typography.Text>Show IP addresses</Typography.Text>
|
||||
<Switch
|
||||
size="small"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
checked={showIP ?? true}
|
||||
value={showIP ?? true}
|
||||
onChange={(checked): void => {
|
||||
logEvent('API Monitoring: Show IP addresses clicked', {
|
||||
showIP: checked,
|
||||
|
||||
@@ -4,7 +4,8 @@ import type {
|
||||
TableColumnsType as ColumnsType,
|
||||
TableColumnType as ColumnType,
|
||||
} from 'antd';
|
||||
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
|
||||
import { Button, Dropdown, Flex, MenuProps } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
@@ -60,9 +61,7 @@ function DynamicColumnTable({
|
||||
|
||||
const onToggleHandler =
|
||||
(index: number, column: ColumnGroupType<any> | ColumnType<any>) =>
|
||||
(checked: boolean, event: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
event.stopPropagation();
|
||||
|
||||
(checked: boolean): void => {
|
||||
if (shouldSendAlertsLogEvent) {
|
||||
logEvent('Alert: Column toggled', {
|
||||
column: column?.title,
|
||||
@@ -88,10 +87,14 @@ function DynamicColumnTable({
|
||||
const items: MenuProps['items'] =
|
||||
dynamicColumns?.map((column, index) => ({
|
||||
label: (
|
||||
<div className="dynamicColumnsTable-items">
|
||||
<div
|
||||
className="dynamicColumnsTable-items"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<div>{column.title?.toString()}</div>
|
||||
<Switch
|
||||
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
|
||||
value={columnsData?.findIndex((c) => c.key === column.key) !== -1}
|
||||
onChange={onToggleHandler(index, column)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { QueryFunctionContext, useQueries, useQuery } from 'react-query';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Switch, Table, Tooltip, Typography } from 'antd';
|
||||
import { Spin, Table, Tooltip, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import { getQueryRangeV5 } from 'api/v5/queryRange/getQueryRange';
|
||||
import { MetricRangePayloadV5, ScalarData } from 'api/v5/v5';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
@@ -170,11 +171,7 @@ function TopErrors({
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Switch
|
||||
checked={showStatusCodeErrors}
|
||||
onChange={setShowStatusCodeErrors}
|
||||
size="small"
|
||||
/>
|
||||
<Switch value={showStatusCodeErrors} onChange={setShowStatusCodeErrors} />
|
||||
<span style={{ color: 'white', fontSize: '14px' }}>
|
||||
Status Message Exists
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, Flex, SelectProps, Switch, Typography } from 'antd';
|
||||
import { Button, Flex, SelectProps, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import type { BaseOptionType, DefaultOptionType } from 'antd/es/select';
|
||||
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
@@ -416,8 +417,8 @@ export function RoutingPolicyBanner({
|
||||
</Typography.Text>
|
||||
<div className="routing-policies-info-banner-right">
|
||||
<Switch
|
||||
checked={notificationSettings.routingPolicies}
|
||||
data-testid="routing-policies-switch"
|
||||
value={notificationSettings.routingPolicies}
|
||||
testId="routing-policies-switch"
|
||||
onChange={(value): void => {
|
||||
setNotificationSettings({
|
||||
type: 'SET_ROUTING_POLICIES',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Switch, Tooltip, Typography } from 'antd';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
import { IAdvancedOptionItemProps } from '../types';
|
||||
@@ -48,7 +49,7 @@ function AdvancedOptionItem({
|
||||
>
|
||||
{input}
|
||||
</div>
|
||||
<Switch onChange={handleOnToggle} checked={showInput} />
|
||||
<Switch onChange={handleOnToggle} value={showInput} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { orange } from '@ant-design/colors';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Collapse, Input, Select, Switch, Tag, Typography } from 'antd';
|
||||
import { Button, Collapse, Input, Select, Tag, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||
import cx from 'classnames';
|
||||
import Editor from 'components/Editor';
|
||||
@@ -762,7 +763,7 @@ function VariableItem({
|
||||
</Typography>
|
||||
</LabelContainer>
|
||||
<Switch
|
||||
checked={variableMultiSelect}
|
||||
value={variableMultiSelect}
|
||||
onChange={(e): void => {
|
||||
setVariableMultiSelect(e);
|
||||
if (!e) {
|
||||
@@ -779,7 +780,7 @@ function VariableItem({
|
||||
</Typography>
|
||||
</LabelContainer>
|
||||
<Switch
|
||||
checked={variableShowALLOption}
|
||||
value={variableShowALLOption}
|
||||
onChange={(e): void => setVariableShowALLOption(e)}
|
||||
/>
|
||||
</VariableItemRow>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, FormInstance, Input, Select, Switch, Typography } from 'antd';
|
||||
import { Form, FormInstance, Input, Select, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import type { Store } from 'antd/lib/form/interface';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
@@ -81,8 +82,8 @@ function FormAlertChannels({
|
||||
name="send_resolved"
|
||||
>
|
||||
<Switch
|
||||
defaultChecked={initialValue?.send_resolved}
|
||||
data-testid="field-send-resolved-checkbox"
|
||||
defaultValue={initialValue?.send_resolved}
|
||||
testId="field-send-resolved-checkbox"
|
||||
onChange={(value): void => {
|
||||
setSelectedConfig((state) => ({
|
||||
...state,
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Select, Switch, Tooltip } from 'antd';
|
||||
import { Button, Form, Select, Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import getAll from 'api/channels/getAll';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
@@ -198,10 +199,10 @@ function BasicInfo({
|
||||
placement="right"
|
||||
>
|
||||
<Switch
|
||||
checked={shouldBroadCastToAllChannels}
|
||||
value={shouldBroadCastToAllChannels}
|
||||
onChange={handleBroadcastToAllChannels}
|
||||
disabled={noChannels || !!isLoading}
|
||||
data-testid="alert-broadcast-to-all-channels"
|
||||
testId="alert-broadcast-to-all-channels"
|
||||
/>
|
||||
</Tooltip>
|
||||
</FormItemMedium>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Switch, Typography } from 'antd';
|
||||
import { Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@@ -227,9 +228,8 @@ function LiveLogsContainer({
|
||||
<div className="live-logs-frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showLiveLogsFrequencyChart}
|
||||
defaultChecked
|
||||
value={showLiveLogsFrequencyChart}
|
||||
defaultValue
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Switch, Typography } from 'antd';
|
||||
import { Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import { JSONViewProps } from './LogDetailedView.types';
|
||||
@@ -81,7 +82,7 @@ function JSONView({ logData }: JSONViewProps): JSX.Element {
|
||||
<div className="log-switch">
|
||||
<div className="wrap-word-switch">
|
||||
<Typography.Text>Wrap text</Typography.Text>
|
||||
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
|
||||
<Switch value={isWrapWord} onChange={handleWrapWord} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { ReactNode, useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { Collapse, Divider, Input, Switch, Tag, Typography } from 'antd';
|
||||
import { Collapse, Divider, Input, Tag, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
@@ -136,7 +137,7 @@ function Overview({
|
||||
<div className="log-switch">
|
||||
<div className="wrap-word-switch">
|
||||
<Typography.Text>Wrap text</Typography.Text>
|
||||
<Switch checked={isWrapWord} onChange={handleWrapWord} size="small" />
|
||||
<Switch value={isWrapWord} onChange={handleWrapWord} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Switch, Typography } from 'antd';
|
||||
import { Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
@@ -68,9 +69,8 @@ function LogsActionsContainer({
|
||||
<div className="frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
value={showFrequencyChart}
|
||||
defaultValue
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Switch, Tooltip } from 'antd';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
@@ -313,10 +314,9 @@ function Explorer(): JSX.Element {
|
||||
title={oneChartPerQueryDisabledTooltip}
|
||||
>
|
||||
<Switch
|
||||
checked={showOneChartPerQuery}
|
||||
value={showOneChartPerQuery}
|
||||
onChange={handleToggleShowOneChartPerQuery}
|
||||
disabled={disableOneChartPerQuery || splitedQueries.length <= 1}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Skeleton, Switch, Typography } from 'antd';
|
||||
import { Button, Skeleton, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -205,7 +206,7 @@ function GraphView({
|
||||
</Button.Group>
|
||||
<div className="view-toggle-button">
|
||||
<Switch
|
||||
checked={viewType === 'graph'}
|
||||
value={viewType === 'graph'}
|
||||
onChange={(checked): void => {
|
||||
const newViewType = checked ? 'graph' : 'table';
|
||||
setViewType(newViewType);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Switch } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Delete } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
@@ -49,10 +49,10 @@ function TimezoneAdaptation(): JSX.Element {
|
||||
<div className="timezone-adaption__header">
|
||||
<h2 className="timezone-adaption__title">Adapt to my timezone</h2>
|
||||
<Switch
|
||||
checked={isAdaptationEnabled}
|
||||
value={isAdaptationEnabled}
|
||||
onChange={handleSwitchChange}
|
||||
style={getSwitchStyles()}
|
||||
data-testid="timezone-adaptation-switch"
|
||||
testId="timezone-adaptation-switch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
|
||||
import { Radio, RadioChangeEvent, Tag } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
||||
@@ -218,10 +219,10 @@ function MySettings(): JSX.Element {
|
||||
<div className="user-preference-section-content-item-title-action">
|
||||
Keep the primary sidebar always open{' '}
|
||||
<Switch
|
||||
checked={sideNavPinned}
|
||||
value={sideNavPinned}
|
||||
onChange={handleSideNavPinnedChange}
|
||||
loading={isUpdatingUserPreference}
|
||||
data-testid="side-nav-pinned-switch"
|
||||
disabled={isUpdatingUserPreference}
|
||||
testId="side-nav-pinned-switch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Switch, Typography } from 'antd';
|
||||
import { Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import {
|
||||
FillMode,
|
||||
LineInterpolation,
|
||||
@@ -72,7 +73,7 @@ export default function ChartAppearanceSection({
|
||||
Display individual data points on the chart
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Switch size="small" checked={showPoints} onChange={setShowPoints} />
|
||||
<Switch value={showPoints} onChange={setShowPoints} />
|
||||
</section>
|
||||
)}
|
||||
{allowSpanGaps && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { InputNumber, Switch, Typography } from 'antd';
|
||||
import { InputNumber, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
|
||||
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||
|
||||
@@ -60,8 +61,7 @@ export default function HistogramBucketsSection({
|
||||
</span>
|
||||
</Typography.Text>
|
||||
<Switch
|
||||
checked={combineHistogram}
|
||||
size="small"
|
||||
value={combineHistogram}
|
||||
onChange={(checked): void => setCombineHistogram(checked)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { Select, Switch, Typography } from 'antd';
|
||||
import { Select, Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
@@ -103,8 +104,7 @@ export default function VisualizationSettingsSection({
|
||||
<section className="stack-chart control-container">
|
||||
<Typography.Text className="section-heading">Stack series</Typography.Text>
|
||||
<Switch
|
||||
checked={stackedBarChart}
|
||||
size="small"
|
||||
value={stackedBarChart}
|
||||
onChange={(checked): void => setStackedBarChart(checked)}
|
||||
/>
|
||||
</section>
|
||||
@@ -119,8 +119,7 @@ export default function VisualizationSettingsSection({
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isFillSpans}
|
||||
size="small"
|
||||
value={isFillSpans}
|
||||
onChange={(checked): void => setIsFillSpans(checked)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -341,10 +341,10 @@ describe('Stacking bar in new panel', () => {
|
||||
const section = container.querySelector('.stack-chart');
|
||||
expect(section).toBeInTheDocument();
|
||||
|
||||
// Verify switch is present and enabled (ant-switch-checked)
|
||||
const switchBtn = section?.querySelector('.ant-switch');
|
||||
// Verify switch is present and enabled
|
||||
const switchBtn = section?.querySelector('[role="switch"]');
|
||||
expect(switchBtn).toBeInTheDocument();
|
||||
expect(switchBtn).toHaveClass('ant-switch-checked');
|
||||
expect(switchBtn).toHaveAttribute('data-state', 'checked');
|
||||
|
||||
// Check that stack series is present and checked
|
||||
checkStackSeriesState(container, true);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Form, Input, Space, Switch, Tooltip } from 'antd';
|
||||
import { Form, Input, Space, Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import { ProcessorData } from 'types/api/pipeline/def';
|
||||
|
||||
import { PREDEFINED_MAPPING } from '../config';
|
||||
@@ -58,11 +59,7 @@ function JsonFlattening({
|
||||
initialValue={isAdd ? true : selectedProcessorData?.enable_paths}
|
||||
>
|
||||
<Space>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enablePaths}
|
||||
onChange={handleEnablePathsChange}
|
||||
/>
|
||||
<Switch value={enablePaths} onChange={handleEnablePathsChange} />
|
||||
Enable Paths
|
||||
</Space>
|
||||
</Form.Item>
|
||||
@@ -79,11 +76,7 @@ function JsonFlattening({
|
||||
|
||||
<Form.Item className="json-flattening-form__item">
|
||||
<Space>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enableMapping}
|
||||
onChange={handleEnableMappingChange}
|
||||
/>
|
||||
<Switch value={enableMapping} onChange={handleEnableMappingChange} />
|
||||
Enable Mapping
|
||||
<Tooltip title="The order of filled keys will determine the priority of keys i.e. earlier keys have higher precedence">
|
||||
<InfoCircleOutlined />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Select, Space, Switch } from 'antd';
|
||||
import { Form, Input, Select, Space } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import { ModalFooterTitle } from 'container/PipelinePage/styles';
|
||||
import { ProcessorData } from 'types/api/pipeline/def';
|
||||
|
||||
@@ -92,8 +93,7 @@ function ProcessorFieldInput({
|
||||
>
|
||||
<Space>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={form.getFieldValue('enable_flattening')}
|
||||
value={form.getFieldValue('enable_flattening')}
|
||||
onChange={(checked: boolean): void => {
|
||||
form.setFieldValue('enable_flattening', checked);
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HolderOutlined } from '@ant-design/icons';
|
||||
import { Switch } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
|
||||
import { holdIconStyle } from '../config';
|
||||
import { LastActionColumn } from '../styles';
|
||||
@@ -7,7 +7,7 @@ import { LastActionColumn } from '../styles';
|
||||
function DragAction({ isEnabled, onChange }: DragActionProps): JSX.Element {
|
||||
return (
|
||||
<LastActionColumn>
|
||||
<Switch defaultChecked={isEnabled} onChange={onChange} />
|
||||
<Switch defaultValue={isEnabled} onChange={onChange} />
|
||||
<HolderOutlined style={holdIconStyle} />
|
||||
</LastActionColumn>
|
||||
);
|
||||
|
||||
@@ -185,7 +185,7 @@ describe('PipelinePage container test', () => {
|
||||
await fireEvent.click(expandIcon[0]);
|
||||
|
||||
const switchToggle = document.querySelector(
|
||||
'.ant-table-expanded-row .ant-switch',
|
||||
'.ant-table-expanded-row [role="switch"]',
|
||||
);
|
||||
|
||||
expect(switchToggle).toBeChecked();
|
||||
@@ -236,7 +236,7 @@ describe('PipelinePage container test', () => {
|
||||
const addNewPipelineBtn = getByText('add_new_pipeline');
|
||||
expect(addNewPipelineBtn).toBeInTheDocument();
|
||||
|
||||
const switchToggle = document.querySelectorAll('.ant-switch');
|
||||
const switchToggle = document.querySelectorAll('[role="switch"]');
|
||||
|
||||
expect(switchToggle[0]).not.toBeChecked();
|
||||
await fireEvent.click(switchToggle[0] as HTMLElement);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PipelinePage container test should render DragAction section 1`] = `
|
||||
<DocumentFragment>
|
||||
@@ -21,26 +21,25 @@ exports[`PipelinePage container test should render DragAction section 1`] = `
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<button
|
||||
aria-checked="true"
|
||||
class="ant-switch css-dev-only-do-not-override-2i2tap ant-switch-checked"
|
||||
role="switch"
|
||||
type="button"
|
||||
<div
|
||||
class="_switch-wrapper_jbsv7_1"
|
||||
>
|
||||
<div
|
||||
class="ant-switch-handle"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner"
|
||||
<button
|
||||
aria-checked="true"
|
||||
class="_switch_jbsv7_1"
|
||||
data-color="robin"
|
||||
data-state="checked"
|
||||
id=":r0:"
|
||||
role="switch"
|
||||
type="button"
|
||||
value="on"
|
||||
>
|
||||
<span
|
||||
class="ant-switch-inner-checked"
|
||||
class="_switch__thumb_jbsv7_59"
|
||||
data-state="checked"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner-unchecked"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
aria-label="holder"
|
||||
class="anticon anticon-holder"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Divider, Dropdown, MenuProps, Switch, Tooltip } from 'antd';
|
||||
import { Divider, Dropdown, MenuProps, Tooltip } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Copy, Ellipsis, PenLine, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
@@ -127,11 +128,7 @@ function AlertActionButtons({
|
||||
<div className="alert-action-buttons">
|
||||
<Tooltip title={isAlertRuleDisabled ? 'Enable alert' : 'Disable alert'}>
|
||||
{isAlertRuleDisabled !== undefined && (
|
||||
<Switch
|
||||
size="small"
|
||||
onChange={toggleAlertRule}
|
||||
checked={!isAlertRuleDisabled}
|
||||
/>
|
||||
<Switch onChange={toggleAlertRule} value={!isAlertRuleDisabled} />
|
||||
)}
|
||||
</Tooltip>
|
||||
<CopyToClipboard textToCopy={window.location.href} />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Switch, Typography } from 'antd';
|
||||
import { Typography } from 'antd';
|
||||
import { Switch } from '@signozhq/ui';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import './JsonView.styles.scss';
|
||||
@@ -65,9 +66,8 @@ function JsonView({ data, height = '575px' }: JsonViewProps): JSX.Element {
|
||||
<div className="json-view__wrap-toggle">
|
||||
<Typography.Text>Wrap text</Typography.Text>
|
||||
<Switch
|
||||
checked={isWrapWord}
|
||||
value={isWrapWord}
|
||||
onChange={(checked): void => setIsWrapWord(checked)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -184,7 +184,7 @@ func (store *store) UpdatePublic(ctx context.Context, storable *dashboardtypes.S
|
||||
func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(dashboardtypes.StorableDashboard)).
|
||||
Where("id = ?", id).
|
||||
@@ -200,7 +200,7 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI
|
||||
func (store *store) DeletePublic(ctx context.Context, dashboardID string) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(dashboardtypes.StorablePublicDashboard)).
|
||||
Where("dashboard_id = ?", dashboardID).
|
||||
|
||||
@@ -147,7 +147,7 @@ func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64,
|
||||
|
||||
count, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(storable).
|
||||
Where("org_id = ?", orgID).
|
||||
|
||||
@@ -59,6 +59,12 @@ def pytest_addoption(parser: pytest.Parser):
|
||||
default="delete",
|
||||
help="sqlite mode",
|
||||
)
|
||||
parser.addoption(
|
||||
"--sqlite-transaction-mode",
|
||||
action="store",
|
||||
default="deferred",
|
||||
help="sqlite transaction mode",
|
||||
)
|
||||
parser.addoption(
|
||||
"--postgres-version",
|
||||
action="store",
|
||||
|
||||
@@ -22,7 +22,9 @@ const storageByUser = new Map<string, Promise<StorageState>>();
|
||||
|
||||
async function storageFor(browser: Browser, user: User): Promise<StorageState> {
|
||||
const cached = storageByUser.get(user.email);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const task = (async () => {
|
||||
const ctx = await browser.newContext();
|
||||
|
||||
34
tests/e2e/helpers/auth.ts
Normal file
34
tests/e2e/helpers/auth.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Browser, BrowserContext } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Build a fresh authenticated `BrowserContext` via UI login. Used by suite
|
||||
* hooks (`test.beforeAll` / `test.afterAll`), where the test-scoped
|
||||
* `authedPage` fixture from `fixtures/auth.ts` is not reachable.
|
||||
*
|
||||
* Each call performs one fresh login (~1s). The per-worker storageState
|
||||
* cache in `fixtures/auth.ts` is intentionally not shared here — keeping
|
||||
* this helper standalone avoids coupling suite hooks to the fixture's
|
||||
* private cache.
|
||||
*/
|
||||
export async function newAdminContext(
|
||||
browser: Browser,
|
||||
): Promise<BrowserContext> {
|
||||
const email = process.env.SIGNOZ_E2E_USERNAME;
|
||||
const password = process.env.SIGNOZ_E2E_PASSWORD;
|
||||
if (!email || !password) {
|
||||
throw new Error(
|
||||
'SIGNOZ_E2E_USERNAME / SIGNOZ_E2E_PASSWORD must be set ' +
|
||||
'(pytest bootstrap writes them to .env.local).',
|
||||
);
|
||||
}
|
||||
const ctx = await browser.newContext();
|
||||
const page = await ctx.newPage();
|
||||
await page.goto('/login?password=Y');
|
||||
await page.getByTestId('email').fill(email);
|
||||
await page.getByTestId('initiate_login').click();
|
||||
await page.getByTestId('password').fill(password);
|
||||
await page.getByRole('button', { name: 'Sign in with Password' }).click();
|
||||
await page.waitForURL((url) => !url.pathname.startsWith('/login'));
|
||||
await page.close();
|
||||
return ctx;
|
||||
}
|
||||
179
tests/e2e/helpers/dashboards.ts
Normal file
179
tests/e2e/helpers/dashboards.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import path from 'path';
|
||||
|
||||
import type { APIRequestContext, Locator, Page } from '@playwright/test';
|
||||
|
||||
import apmMetricsTemplate from '../testdata/apm-metrics.json';
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────
|
||||
//
|
||||
// UI strings and well-known values referenced both within this file and by
|
||||
// specs. Centralised here so a copy-edit in the app updates one place.
|
||||
|
||||
export const DASHBOARDS_LIST_PATH = '/dashboard';
|
||||
export const SEARCH_PLACEHOLDER = 'Search by name, description, or tags...';
|
||||
export const LIST_HEADING = 'Dashboards';
|
||||
|
||||
/** Title the "Create dashboard" dropdown option assigns by default. */
|
||||
export const DEFAULT_DASHBOARD_TITLE = 'Sample Title';
|
||||
|
||||
/** Title of the APM Metrics dashboard imported from the JSON test fixture. */
|
||||
export const APM_METRICS_TITLE = (apmMetricsTemplate as { title: string })
|
||||
.title;
|
||||
|
||||
const APM_METRICS_TESTDATA_PATH = path.resolve(
|
||||
__dirname,
|
||||
'../testdata/apm-metrics.json',
|
||||
);
|
||||
|
||||
// ─── Auth ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Read the JWT the auth fixture stored in `localStorage.AUTH_TOKEN`. The
|
||||
* page must be on the SigNoz origin first; if not, this navigates to the
|
||||
* dashboards list to populate localStorage from the context's storageState.
|
||||
*/
|
||||
export async function authToken(page: Page): Promise<string> {
|
||||
if (!page.url().startsWith('http')) {
|
||||
await page.goto(DASHBOARDS_LIST_PATH);
|
||||
}
|
||||
return page.evaluate(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
() => (globalThis as any).localStorage.getItem('AUTH_TOKEN') || '',
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Navigation ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Navigate to the dashboards list and wait for the page heading. The heading
|
||||
* is the only element guaranteed to render in both the empty zero-state and
|
||||
* the populated list view — search input and sort button are hidden in the
|
||||
* zero-state.
|
||||
*/
|
||||
export async function gotoDashboardsList(page: Page): Promise<void> {
|
||||
await page.goto(DASHBOARDS_LIST_PATH);
|
||||
await page
|
||||
.getByRole('heading', { name: LIST_HEADING, level: 1 })
|
||||
.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
// ─── API helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
async function postDashboard(
|
||||
page: Page,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<string> {
|
||||
const token = await authToken(page);
|
||||
const res = await page.request.post('/api/v1/dashboards', {
|
||||
data: body,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok()) {
|
||||
throw new Error(`POST /dashboards ${res.status()}: ${await res.text()}`);
|
||||
}
|
||||
const json = (await res.json()) as { data: { id: string } };
|
||||
return json.data.id;
|
||||
}
|
||||
|
||||
/** Seed a minimally-named dashboard via API. Returns the new ID. */
|
||||
export async function createDashboardViaApi(
|
||||
page: Page,
|
||||
title: string,
|
||||
): Promise<string> {
|
||||
return postDashboard(page, { title, uploadedGrafana: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the APM Metrics dashboard by driving the real "Import JSON" UI flow:
|
||||
* opens the New-dashboard dropdown, picks Import JSON, uploads the fixture
|
||||
* file, and clicks Import and Next. Returns the new dashboard ID parsed
|
||||
* from the destination URL.
|
||||
*
|
||||
* Pre-condition: the workspace must already be non-empty so the
|
||||
* `new-dashboard-cta` testid is rendered. Seed a minimal base dashboard via
|
||||
* `createDashboardViaApi` first if you're calling this from `beforeAll`.
|
||||
*/
|
||||
export async function importApmMetricsDashboardViaUI(
|
||||
page: Page,
|
||||
): Promise<string> {
|
||||
await gotoDashboardsList(page);
|
||||
await page.getByTestId('new-dashboard-cta').click();
|
||||
await page.getByTestId('import-json-menu-cta').click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await dialog.waitFor({ state: 'visible' });
|
||||
|
||||
// Ant Upload's hidden <input type="file"> — `setInputFiles` drives the
|
||||
// file selection without opening the OS file picker. The component's
|
||||
// `beforeUpload` returns false, so the file is parsed client-side into
|
||||
// the Monaco editor rather than being POSTed by Ant.
|
||||
await dialog
|
||||
.locator('input[type="file"]')
|
||||
.setInputFiles(APM_METRICS_TESTDATA_PATH);
|
||||
|
||||
await dialog.getByRole('button', { name: 'Import and Next' }).click();
|
||||
|
||||
await page.waitForURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
const match = page.url().match(/\/dashboard\/([0-9a-f-]+)/);
|
||||
if (!match) {
|
||||
throw new Error(`Expected dashboard ID in URL, got: ${page.url()}`);
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort delete via API. Errors are swallowed so suite-level cleanup
|
||||
* stays resilient when a UI flow already deleted the resource (404) or the
|
||||
* stack is mid-shutdown.
|
||||
*/
|
||||
export async function deleteDashboardViaApi(
|
||||
request: APIRequestContext,
|
||||
id: string,
|
||||
token: string,
|
||||
): Promise<void> {
|
||||
await request
|
||||
.delete(`/api/v1/dashboards/${id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
/** Look up a dashboard ID by exact title via the list API. */
|
||||
export async function findDashboardIdByTitle(
|
||||
page: Page,
|
||||
title: string,
|
||||
): Promise<string | undefined> {
|
||||
const token = await authToken(page);
|
||||
const res = await page.request.get('/api/v1/dashboards', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok()) {
|
||||
return undefined;
|
||||
}
|
||||
const body = (await res.json()) as {
|
||||
data: Array<{ id: string; data: { title: string } }>;
|
||||
};
|
||||
return body.data.find((d) => d.data.title === title)?.id;
|
||||
}
|
||||
|
||||
// ─── List page UI helpers ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Filter the list to a specific dashboard by name and open its row action
|
||||
* menu. Returns the tooltip locator that wraps View / Open in New Tab /
|
||||
* Copy Link / Export JSON / Delete dashboard.
|
||||
*
|
||||
* The action icon scrolls out of the viewport when the matching row lands
|
||||
* below the table's sticky header — `scrollIntoViewIfNeeded` keeps the
|
||||
* click reliable regardless of how many other dashboards exist.
|
||||
*/
|
||||
export async function openDashboardActionMenu(
|
||||
page: Page,
|
||||
dashboardName: string,
|
||||
): Promise<Locator> {
|
||||
await page.getByPlaceholder(SEARCH_PLACEHOLDER).fill(dashboardName);
|
||||
const icon = page.getByTestId('dashboard-action-icon').first();
|
||||
await icon.scrollIntoViewIfNeeded();
|
||||
await icon.click();
|
||||
return page.getByRole('tooltip');
|
||||
}
|
||||
3639
tests/e2e/testdata/apm-metrics.json
vendored
Normal file
3639
tests/e2e/testdata/apm-metrics.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
570
tests/e2e/tests/dashboards/list.spec.ts
Normal file
570
tests/e2e/tests/dashboards/list.spec.ts
Normal file
@@ -0,0 +1,570 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from '../../fixtures/auth';
|
||||
import { newAdminContext } from '../../helpers/auth';
|
||||
import {
|
||||
APM_METRICS_TITLE,
|
||||
authToken,
|
||||
createDashboardViaApi,
|
||||
DEFAULT_DASHBOARD_TITLE,
|
||||
deleteDashboardViaApi,
|
||||
findDashboardIdByTitle,
|
||||
gotoDashboardsList,
|
||||
importApmMetricsDashboardViaUI,
|
||||
openDashboardActionMenu,
|
||||
SEARCH_PLACEHOLDER,
|
||||
} from '../../helpers/dashboards';
|
||||
|
||||
// Tests in this file mutate the dashboard list (create / delete). Run them
|
||||
// serially within the worker so state from one test does not leak into
|
||||
// another's assertions. Files still run in parallel via the project-level
|
||||
// fullyParallel setting.
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
// ─── Suite-level seed registry ───────────────────────────────────────────
|
||||
//
|
||||
// Every dashboard a test creates is recorded here, and one `afterAll`
|
||||
// deletes the lot at suite teardown. Individual tests do not need their
|
||||
// own `try / finally` cleanup blocks.
|
||||
const seedIds = new Set<string>();
|
||||
const BASE_FIXTURE_TITLE = 'dashboards-list-base-fixture';
|
||||
|
||||
/** Seed a dashboard via API and register it for suite cleanup. */
|
||||
async function seed(page: Page, title: string): Promise<string> {
|
||||
const id = await createDashboardViaApi(page, title);
|
||||
seedIds.add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
// Persistent fixtures the read-only tests rely on:
|
||||
// - A minimal base dashboard — keeps the list non-empty so the search
|
||||
// input / sort button render. Seeded first via API so the workspace
|
||||
// is populated before the UI import flow runs.
|
||||
// - APM Metrics — a richer, real-world dashboard imported through the
|
||||
// real Import JSON UI flow (file upload + Monaco editor + submit).
|
||||
const ctx = await newAdminContext(browser);
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
seedIds.add(await createDashboardViaApi(page, BASE_FIXTURE_TITLE));
|
||||
seedIds.add(await importApmMetricsDashboardViaUI(page));
|
||||
} finally {
|
||||
await ctx.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
if (seedIds.size === 0) {
|
||||
return;
|
||||
}
|
||||
const ctx = await newAdminContext(browser);
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
const token = await authToken(page);
|
||||
for (const id of seedIds) {
|
||||
await deleteDashboardViaApi(ctx.request, id, token);
|
||||
seedIds.delete(id);
|
||||
}
|
||||
} finally {
|
||||
await ctx.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('Dashboards List Page', () => {
|
||||
// ─── Page load and layout ────────────────────────────────────────────────
|
||||
|
||||
test('TC-01 page chrome and core controls render', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await gotoDashboardsList(page);
|
||||
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
await expect(page).toHaveTitle('SigNoz | All Dashboards');
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Dashboards', level: 1 }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('Create and manage dashboards for your workspace.'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByPlaceholder(SEARCH_PLACEHOLDER)).toBeVisible();
|
||||
await expect(page.getByText('All Dashboards')).toBeVisible();
|
||||
await expect(page.getByTestId('sort-by')).toBeVisible();
|
||||
|
||||
await expect(page.getByAltText('dashboard-image').first()).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Feedback' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
});
|
||||
|
||||
// ─── Search functionality ────────────────────────────────────────────────
|
||||
|
||||
test('TC-02 search by title returns matching dashboard', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-search-title';
|
||||
await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
|
||||
|
||||
await search.fill(name);
|
||||
await expect(page).toHaveURL(new RegExp(`search=${name}`));
|
||||
await expect(search).toHaveValue(name);
|
||||
await expect(page.getByAltText('dashboard-image').first()).toBeVisible();
|
||||
await expect(page.getByText(name).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-03 search by tag returns the APM Metrics dashboard', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
// APM Metrics carries multiple tags — searching by one of them ("apm")
|
||||
// surfaces the imported dashboard. This exercises the tag-match branch
|
||||
// in the filter, distinct from title-match.
|
||||
await gotoDashboardsList(page);
|
||||
const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
|
||||
|
||||
await search.fill('apm');
|
||||
await expect(page).toHaveURL(/search=apm/);
|
||||
await expect(page.getByText(APM_METRICS_TITLE).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-04 direct navigation with ?search= pre-fills the input and filters results', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-search-deeplink';
|
||||
await seed(page, name);
|
||||
|
||||
await page.goto(`/dashboard?search=${name}`);
|
||||
await page
|
||||
.getByRole('heading', { name: 'Dashboards', level: 1 })
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await expect(page.getByPlaceholder(SEARCH_PLACEHOLDER)).toHaveValue(name);
|
||||
await expect(page.getByText(name).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-05 clearing search restores the full list', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await gotoDashboardsList(page);
|
||||
const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
|
||||
|
||||
await search.fill('apm');
|
||||
await expect(page).toHaveURL(/search=apm/);
|
||||
|
||||
await search.fill('');
|
||||
// The app keeps the empty `search=` param in the URL — assert that no
|
||||
// non-empty value remains and that rows are rendered again.
|
||||
await expect(page).not.toHaveURL(/search=[^&]/);
|
||||
await expect(search).toHaveValue('');
|
||||
await expect(page.getByAltText('dashboard-image').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-06 search with no matching results shows empty state', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await gotoDashboardsList(page);
|
||||
const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
|
||||
|
||||
await search.fill('xyznonexistent999');
|
||||
|
||||
await expect(page.getByAltText('dashboard-image')).toHaveCount(0);
|
||||
await expect(search).toBeVisible();
|
||||
await expect(search).toHaveValue('xyznonexistent999');
|
||||
});
|
||||
|
||||
test('TC-07 search is case-insensitive', async ({ authedPage: page }) => {
|
||||
await gotoDashboardsList(page);
|
||||
const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
|
||||
|
||||
await search.fill(APM_METRICS_TITLE.toLowerCase());
|
||||
await expect(page.getByAltText('dashboard-image').first()).toBeVisible();
|
||||
await expect(page.getByText(APM_METRICS_TITLE).first()).toBeVisible();
|
||||
});
|
||||
|
||||
// ─── Sorting ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// `sortHandle` in DashboardsList.tsx hard-codes `order: 'descend'` —
|
||||
// ascending mode is not yet implemented. Both sort options ride the same
|
||||
// descending-only path, so one parameterised test covers them.
|
||||
|
||||
test('TC-08 sort options write columnKey & order=descend to the URL', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
for (const [optionTestId, columnKey] of [
|
||||
['sort-by-last-updated', 'updatedAt'],
|
||||
['sort-by-last-created', 'createdAt'],
|
||||
] as const) {
|
||||
await gotoDashboardsList(page);
|
||||
await expect(page).not.toHaveURL(/columnKey/);
|
||||
|
||||
await page.getByTestId('sort-by').click();
|
||||
const option = page.getByTestId(optionTestId);
|
||||
await option.waitFor({ state: 'visible' });
|
||||
await option.click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`columnKey=${columnKey}`));
|
||||
await expect(page).toHaveURL(/order=descend/);
|
||||
await expect(page).not.toHaveURL(/order=ascend/);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Row actions (context menu) ──────────────────────────────────────────
|
||||
|
||||
test('TC-09 admin sees all five options in the action menu', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-actions-menu';
|
||||
await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
const tooltip = await openDashboardActionMenu(page, name);
|
||||
await expect(tooltip).toBeVisible();
|
||||
|
||||
await expect(tooltip.getByRole('button', { name: 'View' })).toBeVisible();
|
||||
await expect(
|
||||
tooltip.getByRole('button', { name: 'Open in New Tab' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
tooltip.getByRole('button', { name: 'Copy Link' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
tooltip.getByRole('button', { name: 'Export JSON' }),
|
||||
).toBeVisible();
|
||||
// Delete is rendered as a generic, not a button.
|
||||
await expect(tooltip.getByText('Delete dashboard')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-10 view action navigates to the dashboard detail page', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-action-view';
|
||||
await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
const tooltip = await openDashboardActionMenu(page, name);
|
||||
await tooltip.getByRole('button', { name: 'View' }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
});
|
||||
|
||||
test('TC-11 open in new tab opens the dashboard in a new browser tab', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-action-newtab';
|
||||
await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
const tooltip = await openDashboardActionMenu(page, name);
|
||||
|
||||
// Use page.context() — the auth fixture creates its own context per
|
||||
// test, which is not the same as the default `context` fixture.
|
||||
const [newPage] = await Promise.all([
|
||||
page.context().waitForEvent('page'),
|
||||
tooltip.getByRole('button', { name: 'Open in New Tab' }).click(),
|
||||
]);
|
||||
|
||||
await newPage.waitForLoadState();
|
||||
await expect(newPage).toHaveURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
await newPage.close();
|
||||
});
|
||||
|
||||
test('TC-12 copy link copies the dashboard URL to the clipboard', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-action-copy';
|
||||
await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
|
||||
const tooltip = await openDashboardActionMenu(page, name);
|
||||
await tooltip.getByRole('button', { name: 'Copy Link' }).click();
|
||||
|
||||
await expect(page.getByText(/copied|success/i)).toBeVisible();
|
||||
|
||||
const clipboardText = await page.evaluate(async () =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).navigator.clipboard.readText(),
|
||||
);
|
||||
expect(clipboardText).toMatch(/\/dashboard\/[0-9a-f-]+/);
|
||||
});
|
||||
|
||||
test('TC-13 export JSON downloads the dashboard as a JSON file', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-action-export';
|
||||
await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
const tooltip = await openDashboardActionMenu(page, name);
|
||||
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
tooltip.getByRole('button', { name: 'Export JSON' }).click(),
|
||||
]);
|
||||
|
||||
expect(download.suggestedFilename()).toMatch(/\.json$/);
|
||||
});
|
||||
|
||||
test('TC-14 action menu closes when clicking outside the popover', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-action-dismiss';
|
||||
await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
await openDashboardActionMenu(page, name);
|
||||
await expect(page.getByRole('tooltip')).toBeVisible();
|
||||
|
||||
await page.getByRole('heading', { name: 'Dashboards', level: 1 }).click();
|
||||
await expect(page.getByRole('tooltip')).not.toBeVisible();
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard($|\?)/);
|
||||
});
|
||||
|
||||
// ─── Creating dashboards via "New dashboard" dropdown ─────────────────────
|
||||
//
|
||||
// The "Enter dashboard name…" inline input on the list page is a
|
||||
// `RequestDashboardBtn` (template-request feedback form), not a create
|
||||
// flow. The only UI create path is the "New dashboard" dropdown.
|
||||
|
||||
test('TC-15 New dashboard dropdown shows exactly three options', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await gotoDashboardsList(page);
|
||||
await page.getByTestId('new-dashboard-cta').click();
|
||||
|
||||
const menu = page.getByRole('menu');
|
||||
await expect(menu).toBeVisible();
|
||||
await expect(menu.getByTestId('create-dashboard-menu-cta')).toBeVisible();
|
||||
await expect(menu.getByTestId('import-json-menu-cta')).toBeVisible();
|
||||
await expect(menu.getByTestId('view-templates-menu-cta')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-16 Create dashboard dropdown option creates a dashboard with the default name', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await gotoDashboardsList(page);
|
||||
await page.getByTestId('new-dashboard-cta').click();
|
||||
await page.getByTestId('create-dashboard-menu-cta').click();
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
await expect(page.getByText('Configure your new dashboard')).toBeVisible();
|
||||
// "Configure" appears twice on the new-dashboard onboarding state — once
|
||||
// in the toolbar and once in the empty-state section. The test only
|
||||
// needs to confirm the onboarding rendered, so .first() is sufficient.
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Configure' }).first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: /New Panel/ }).first(),
|
||||
).toBeVisible();
|
||||
|
||||
// Register the UI-created dashboard with the suite teardown. After a
|
||||
// successful "Create dashboard" the row must exist — assert that and
|
||||
// then unconditionally register, so the test contains no `if`.
|
||||
const sampleId = await findDashboardIdByTitle(page, DEFAULT_DASHBOARD_TITLE);
|
||||
expect(
|
||||
sampleId,
|
||||
`${DEFAULT_DASHBOARD_TITLE} not found after UI create`,
|
||||
).toBeDefined();
|
||||
seedIds.add(sampleId as string);
|
||||
});
|
||||
|
||||
test('TC-17 Import JSON dialog opens with code editor and upload button', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await gotoDashboardsList(page);
|
||||
await page.getByTestId('new-dashboard-cta').click();
|
||||
await page.getByTestId('import-json-menu-cta').click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.getByText('Import Dashboard JSON')).toBeVisible();
|
||||
// "Upload JSON file" appears twice — once as the Ant Upload's hidden
|
||||
// span wrapper, once as the visible button. .first() is enough to
|
||||
// confirm the upload affordance rendered.
|
||||
await expect(
|
||||
dialog.getByRole('button', { name: 'Upload JSON file' }).first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dialog.getByRole('button', { name: 'Import and Next' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-18 Import JSON dialog dismisses via Escape and via the close button', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await gotoDashboardsList(page);
|
||||
|
||||
// Escape path — Monaco grabs focus on mount and swallows Escape; click
|
||||
// the modal title first to blur Monaco so Ant's Modal `keyboard`
|
||||
// handler picks up the keystroke.
|
||||
await page.getByTestId('new-dashboard-cta').click();
|
||||
await page.getByTestId('import-json-menu-cta').click();
|
||||
let dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog.getByText('Import Dashboard JSON').click();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(dialog).not.toBeVisible();
|
||||
await expect(page).toHaveURL(/\/dashboard($|\?)/);
|
||||
|
||||
// Close-button path — re-open and dismiss via the X.
|
||||
await page.getByTestId('new-dashboard-cta').click();
|
||||
await page.getByTestId('import-json-menu-cta').click();
|
||||
dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await dialog.getByRole('button', { name: /close/i }).click();
|
||||
await expect(dialog).not.toBeVisible();
|
||||
await expect(page).toHaveURL(/\/dashboard($|\?)/);
|
||||
});
|
||||
|
||||
// ─── Deleting dashboards ─────────────────────────────────────────────────
|
||||
//
|
||||
// Known behaviour: clicking Cancel in the confirmation dialog navigates to
|
||||
// the dashboard detail page rather than staying on the list.
|
||||
|
||||
test('TC-19 delete confirmation dialog shows dashboard name with Cancel and Delete buttons', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-delete-confirm';
|
||||
await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
const tooltip = await openDashboardActionMenu(page, name);
|
||||
// Ant's Popover can position the tooltip so the "Delete dashboard"
|
||||
// item ends up outside the viewport (especially in CI, where font
|
||||
// rendering shifts layout subtly). `click({ force: true })` skips
|
||||
// actionability checks but Playwright still requires the click
|
||||
// coordinates to land inside the viewport. `dispatchEvent('click')`
|
||||
// fires the synthetic event directly on the DOM node — React's
|
||||
// onClick handler runs normally — and bypasses coordinate checks
|
||||
// entirely. This is the robust fix for Ant Popover positioning.
|
||||
await tooltip.getByText('Delete dashboard').dispatchEvent('click');
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.getByRole('heading')).toContainText(
|
||||
'Are you sure you want to delete the',
|
||||
);
|
||||
await expect(dialog.getByRole('heading')).toContainText(name);
|
||||
|
||||
await expect(dialog.getByRole('button', { name: 'Cancel' })).toBeVisible();
|
||||
await expect(dialog.getByRole('button', { name: 'Delete' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-20 cancelling delete navigates to the dashboard detail page (known behaviour)', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-delete-cancel';
|
||||
await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
const tooltip = await openDashboardActionMenu(page, name);
|
||||
await tooltip.getByText('Delete dashboard').dispatchEvent('click');
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
await expect(page).toHaveURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
});
|
||||
|
||||
test('TC-21 confirming delete removes the dashboard from the list', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-delete-confirmed';
|
||||
const id = await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
const tooltip = await openDashboardActionMenu(page, name);
|
||||
await tooltip.getByText('Delete dashboard').dispatchEvent('click');
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// The Delete mutation is async — wait for the API response *and* the
|
||||
// dialog to dismiss before navigating away, otherwise React Query's
|
||||
// in-flight mutation gets cancelled by the navigation.
|
||||
const deleteResponse = page.waitForResponse(
|
||||
(r) => r.request().method() === 'DELETE' && /\/dashboards\//.test(r.url()),
|
||||
);
|
||||
await dialog.getByRole('button', { name: 'Delete' }).click();
|
||||
await deleteResponse;
|
||||
await expect(dialog).not.toBeVisible();
|
||||
|
||||
// After deletion, searching for the name should return no results.
|
||||
await gotoDashboardsList(page);
|
||||
await page.getByPlaceholder(SEARCH_PLACEHOLDER).fill(name);
|
||||
await expect(page.getByAltText('dashboard-image')).toHaveCount(0);
|
||||
|
||||
// The UI delete already removed the resource — drop it from the
|
||||
// suite-cleanup set so afterAll doesn't 404 on it.
|
||||
seedIds.delete(id);
|
||||
});
|
||||
|
||||
// ─── Row click navigation ────────────────────────────────────────────────
|
||||
|
||||
test('TC-22 clicking a dashboard row navigates to the detail page', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-row-click';
|
||||
await seed(page, name);
|
||||
|
||||
await gotoDashboardsList(page);
|
||||
await page.getByPlaceholder(SEARCH_PLACEHOLDER).fill(name);
|
||||
|
||||
await page.getByAltText('dashboard-image').first().click();
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
});
|
||||
|
||||
test('TC-23 sidebar Dashboards link navigates to the list page', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await page.goto('/home');
|
||||
// Sidebar items are <div class="nav-item"> with the label as visible
|
||||
// text — they're not <a role="link">, so getByRole won't reach them.
|
||||
// Filter on the exact label to avoid matching nested items that
|
||||
// happen to contain the substring.
|
||||
await page
|
||||
.locator('.nav-item')
|
||||
.filter({ hasText: /^Dashboards$/ })
|
||||
.click();
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
await expect(page).toHaveTitle('SigNoz | All Dashboards');
|
||||
});
|
||||
|
||||
// ─── URL state and deep linking ──────────────────────────────────────────
|
||||
|
||||
test('TC-24 browser Back after navigating to a dashboard restores search state', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-back-search';
|
||||
await seed(page, name);
|
||||
|
||||
await page.goto(`/dashboard?search=${name}`);
|
||||
await page
|
||||
.getByRole('heading', { name: 'Dashboards', level: 1 })
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await page.getByAltText('dashboard-image').first().click();
|
||||
await expect(page).toHaveURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
|
||||
await page.goBack();
|
||||
await expect(page).toHaveURL(new RegExp(`search=${name}`));
|
||||
await expect(page.getByPlaceholder(SEARCH_PLACEHOLDER)).toHaveValue(name);
|
||||
});
|
||||
|
||||
test('TC-25 direct navigation with sort params honours them on load', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await page.goto('/dashboard?columnKey=updatedAt&order=descend');
|
||||
await page
|
||||
.getByRole('heading', { name: 'Dashboards', level: 1 })
|
||||
.waitFor({ state: 'visible' });
|
||||
await expect(page).toHaveURL(/columnKey=updatedAt/);
|
||||
await expect(page).toHaveURL(/order=descend/);
|
||||
});
|
||||
});
|
||||
@@ -12,12 +12,12 @@
|
||||
"types": ["node", "@playwright/test"],
|
||||
"paths": {
|
||||
"@tests/*": ["./tests/*"],
|
||||
"@utils/*": ["./utils/*"],
|
||||
"@helpers/*": ["./helpers/*"],
|
||||
"@specs/*": ["./specs/*"]
|
||||
},
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["tests/**/*.ts", "utils/**/*.ts", "playwright.config.ts"],
|
||||
"include": ["tests/**/*.ts", "helpers/**/*.ts", "fixtures/**/*.ts", "playwright.config.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
2
tests/fixtures/sqlite.py
vendored
2
tests/fixtures/sqlite.py
vendored
@@ -30,6 +30,7 @@ def sqlite(
|
||||
assert result.fetchone()[0] == 1
|
||||
|
||||
mode = pytestconfig.getoption("--sqlite-mode")
|
||||
transaction_mode = pytestconfig.getoption("--sqlite-transaction-mode")
|
||||
return types.TestContainerSQL(
|
||||
container=types.TestContainerDocker(
|
||||
id="",
|
||||
@@ -41,6 +42,7 @@ def sqlite(
|
||||
"SIGNOZ_SQLSTORE_PROVIDER": "sqlite",
|
||||
"SIGNOZ_SQLSTORE_SQLITE_PATH": str(path),
|
||||
"SIGNOZ_SQLSTORE_SQLITE_MODE": mode,
|
||||
"SIGNOZ_SQLSTORE_SQLITE_TRANSACTION__MODE": transaction_mode,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user