mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-01 14:50:29 +01:00
Compare commits
12 Commits
chore/play
...
e2e/dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7224fc61dc | ||
|
|
0a346605c7 | ||
|
|
33a7789f9a | ||
|
|
c3762b789d | ||
|
|
cff88f9d9f | ||
|
|
4b3518cb9e | ||
|
|
bd99b637f3 | ||
|
|
46844cf091 | ||
|
|
1f63ebff14 | ||
|
|
ed463a87e4 | ||
|
|
9cba7e88ec | ||
|
|
e4949379e2 |
13
.github/workflows/e2eci.yaml
vendored
13
.github/workflows/e2eci.yaml
vendored
@@ -68,22 +68,9 @@ jobs:
|
||||
- name: pnpm-install
|
||||
run: |
|
||||
cd tests/e2e && pnpm install --frozen-lockfile
|
||||
- name: playwright-cache
|
||||
id: playwright-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('tests/e2e/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
- name: playwright-browsers
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd tests/e2e && pnpm playwright install --with-deps ${{ matrix.project }}
|
||||
- name: playwright-system-deps
|
||||
if: steps.playwright-cache.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
cd tests/e2e && pnpm playwright install-deps ${{ matrix.project }}
|
||||
- name: bring-up-stack
|
||||
run: |
|
||||
cd tests && \
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
} from '@playwright/test';
|
||||
|
||||
import apmMetricsTemplate from '../testdata/apm-metrics.json';
|
||||
import queriesData from '../testdata/queries.json';
|
||||
|
||||
export type SignalType = 'metrics' | 'logs' | 'traces';
|
||||
export type QueriesData = typeof queriesData;
|
||||
import chartDataTemplate from '../testdata/chart-data-dashboard.json';
|
||||
import variablesTemplate from '../testdata/variables-dashboard.json';
|
||||
|
||||
@@ -366,6 +370,36 @@ export async function findDashboardIdByTitle(
|
||||
return body.data.find((d) => d.data.title === title)?.id;
|
||||
}
|
||||
|
||||
/** Shape of the persisted dashboard payload returned by GET /api/v1/dashboards/<id>. */
|
||||
export interface DashboardData {
|
||||
title: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
widgets?: Array<{ id?: string; title?: string }>;
|
||||
variables?: Record<string, Record<string, unknown>>;
|
||||
layout?: unknown[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the persisted dashboard payload via API. Use this for "did the save
|
||||
* actually land on the server?" assertions — UI-only checks can pass on
|
||||
* optimistic-update bugs.
|
||||
*/
|
||||
export async function fetchDashboardData(
|
||||
page: Page,
|
||||
id: string,
|
||||
): Promise<DashboardData> {
|
||||
const token = await authToken(page);
|
||||
const res = await page.request.get(`/api/v1/dashboards/${id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok()) {
|
||||
throw new Error(`GET /dashboards/${id} ${res.status()}: ${await res.text()}`);
|
||||
}
|
||||
const body = (await res.json()) as { data: { data: DashboardData } };
|
||||
return body.data.data;
|
||||
}
|
||||
|
||||
// ─── List page UI helpers ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -387,3 +421,142 @@ export async function openDashboardActionMenu(
|
||||
await icon.click();
|
||||
return page.getByRole('tooltip');
|
||||
}
|
||||
|
||||
// ─── Dashboard detail page helpers ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Click the Configure button (`data-testid="show-drawer"`) on a dashboard
|
||||
* detail page and wait for the settings drawer (`.settings-container-root`) to
|
||||
* be visible. Works from both the empty-state view and the populated toolbar —
|
||||
* both render the same testid.
|
||||
*
|
||||
* Returns the drawer locator so callers can scope further assertions to it.
|
||||
*/
|
||||
export async function openDashboardSettingsDrawer(
|
||||
page: Page,
|
||||
): Promise<Locator> {
|
||||
await page.getByTestId('show-drawer').first().click();
|
||||
const drawer = page.locator('.settings-container-root');
|
||||
await drawer.waitFor({ state: 'visible' });
|
||||
return drawer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click `data-testid="save-dashboard-config"` and wait for the resulting
|
||||
* `PUT /api/v1/dashboards/<id>` response. The Save button is only rendered
|
||||
* when there is at least one unsaved change — callers must ensure the drawer
|
||||
* has been dirtied before calling this.
|
||||
*/
|
||||
export async function saveDashboardSettings(page: Page): Promise<void> {
|
||||
const patchResponse = page.waitForResponse(
|
||||
(r) =>
|
||||
r.request().method() === 'PUT' && /\/api\/v1\/dashboards\//.test(r.url()),
|
||||
);
|
||||
await page.getByTestId('save-dashboard-config').click();
|
||||
await patchResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a dashboard via the toolbar options popover:
|
||||
* opens the popover (`data-testid="options"`), clicks "Rename", fills the
|
||||
* input, clicks "Rename Dashboard", and waits for the PUT response.
|
||||
*
|
||||
* Pre-condition: the caller must be on the dashboard detail page.
|
||||
*/
|
||||
export async function renameDashboardViaToolbar(
|
||||
page: Page,
|
||||
newTitle: string,
|
||||
): Promise<void> {
|
||||
await page.getByTestId('options').click();
|
||||
await page.getByRole('button', { name: 'Rename' }).click();
|
||||
|
||||
const modal = page.getByRole('dialog');
|
||||
await modal.waitFor({ state: 'visible' });
|
||||
|
||||
const input = modal.getByTestId('dashboard-name');
|
||||
await input.clear();
|
||||
await input.fill(newTitle);
|
||||
|
||||
const patchResponse = page.waitForResponse(
|
||||
(r) =>
|
||||
r.request().method() === 'PUT' && /\/api\/v1\/dashboards\//.test(r.url()),
|
||||
);
|
||||
await page.getByRole('button', { name: 'Rename Dashboard' }).click();
|
||||
await patchResponse;
|
||||
|
||||
await modal.waitFor({ state: 'hidden' });
|
||||
}
|
||||
|
||||
// ─── Add panel flow ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* From the dashboard detail page (must already be loaded), drive the full
|
||||
* "Add Panel" flow for the given signal type:
|
||||
* 1. Click the empty-state `add-panel` CTA to open the New Panel modal.
|
||||
* 2. Pick the Time Series panel type.
|
||||
* 3. Fill the panel name in the right pane (drives the post-save assertion).
|
||||
* 4. For metrics: type the metric name from `queries.json` into the metric
|
||||
* AutoComplete and select it from the dropdown. For logs/traces: switch
|
||||
* the data-source selector to LOGS / TRACES; default Query Builder state
|
||||
* is sufficient (queries.json query strings are empty by design).
|
||||
* 5. Click Save Changes and wait for the PUT /api/v1/dashboards/<id> response.
|
||||
*
|
||||
* Throws if the PUT response is not 2xx. After return, the page is back on
|
||||
* the dashboard detail page; the caller asserts the panel rendered.
|
||||
*/
|
||||
export async function configureAndSavePanel(
|
||||
page: Page,
|
||||
signal: SignalType,
|
||||
panelTitle: string,
|
||||
): Promise<void> {
|
||||
await page.getByTestId('add-panel').click();
|
||||
|
||||
const newPanelModal = page
|
||||
.getByRole('dialog')
|
||||
.filter({ hasText: 'New Panel' });
|
||||
await newPanelModal.waitFor({ state: 'visible' });
|
||||
await newPanelModal.getByTestId('panel-type-graph').click();
|
||||
|
||||
await page.getByTestId('new-widget-save').waitFor({ state: 'visible' });
|
||||
await page.getByTestId('panel-name-input').fill(panelTitle);
|
||||
|
||||
if (signal === 'metrics') {
|
||||
const metricName = queriesData.metrics.metricName;
|
||||
// The testid is on the Ant Select wrapper <div>; the editable input
|
||||
// lives inside it. Target the descendant input for fill().
|
||||
const metricInput = page
|
||||
.getByTestId('metric-name-selector-0')
|
||||
.locator('input');
|
||||
await metricInput.click();
|
||||
await metricInput.fill(metricName);
|
||||
// AutoComplete debounces and fetches; wait for the option then click.
|
||||
await page
|
||||
.locator('.ant-select-item-option-content', { hasText: metricName })
|
||||
.first()
|
||||
.click();
|
||||
} else {
|
||||
// logs / traces — switch the data source. Default query is sufficient.
|
||||
await page.getByTestId('query-data-source-selector-0').click();
|
||||
await page
|
||||
.locator('.ant-select-item-option-content', {
|
||||
hasText: signal.toUpperCase(),
|
||||
})
|
||||
.click();
|
||||
}
|
||||
|
||||
const putResponse = page.waitForResponse(
|
||||
(r) =>
|
||||
r.request().method() === 'PUT' && /\/api\/v1\/dashboards\//.test(r.url()),
|
||||
);
|
||||
await page.getByTestId('new-widget-save').click();
|
||||
|
||||
const res = await putResponse;
|
||||
if (!res.ok()) {
|
||||
throw new Error(
|
||||
`PUT /api/v1/dashboards failed ${res.status()}: ${await res.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Save navigates back to /dashboard/<id> (no /new suffix).
|
||||
await page.waitForURL(/\/dashboard\/[0-9a-f-]+(?:\?|$)/);
|
||||
}
|
||||
|
||||
12
tests/e2e/testdata/queries.json
vendored
Normal file
12
tests/e2e/testdata/queries.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"logs": {
|
||||
"query": ""
|
||||
},
|
||||
"metrics": {
|
||||
"metricName": "signoz_calls_total",
|
||||
"query": ""
|
||||
},
|
||||
"traces": {
|
||||
"query": ""
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { expect, test } from '../../../fixtures/auth';
|
||||
import { newAdminContext } from '../../../helpers/auth';
|
||||
import {
|
||||
authToken,
|
||||
configureAndSavePanel,
|
||||
createDashboardViaApi,
|
||||
deleteDashboardViaApi,
|
||||
} from '../../../helpers/dashboards';
|
||||
@@ -143,4 +144,59 @@ test.describe('Dashboard Detail — Add Panel (entry-point + persistence)', () =
|
||||
page.getByText(panelName, { exact: true }).first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
// ─── Per-signal panel creation ───────────────────────────────────────────
|
||||
//
|
||||
// Configure a query for each signal using values from testdata/queries.json,
|
||||
// save the panel, return to the dashboard, and verify the panel card renders
|
||||
// and survives a reload.
|
||||
|
||||
test('TC-03 add metrics Time Series panel using signoz_calls_total from queries.json', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const id = await createDashboardViaApi(page, 'add-panel-metrics');
|
||||
seedIds.add(id);
|
||||
await page.goto(`/dashboard/${id}`);
|
||||
await expect(page.getByTestId('add-panel')).toBeVisible();
|
||||
|
||||
await configureAndSavePanel(page, 'metrics', 'metrics-timeseries');
|
||||
|
||||
await expect(page.getByTestId('metrics-timeseries')).toBeVisible();
|
||||
|
||||
// Reload — proves the panel persists, not just optimistic UI from the save.
|
||||
await page.reload();
|
||||
await expect(page.getByTestId('metrics-timeseries')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-04 add logs Time Series panel with default query from queries.json', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const id = await createDashboardViaApi(page, 'add-panel-logs');
|
||||
seedIds.add(id);
|
||||
await page.goto(`/dashboard/${id}`);
|
||||
await expect(page.getByTestId('add-panel')).toBeVisible();
|
||||
|
||||
await configureAndSavePanel(page, 'logs', 'logs-timeseries');
|
||||
|
||||
await expect(page.getByTestId('logs-timeseries')).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByTestId('logs-timeseries')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-05 add traces Time Series panel with default query from queries.json', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const id = await createDashboardViaApi(page, 'add-panel-traces');
|
||||
seedIds.add(id);
|
||||
await page.goto(`/dashboard/${id}`);
|
||||
await expect(page.getByTestId('add-panel')).toBeVisible();
|
||||
|
||||
await configureAndSavePanel(page, 'traces', 'traces-timeseries');
|
||||
|
||||
await expect(page.getByTestId('traces-timeseries')).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByTestId('traces-timeseries')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
awaitVariablesResolved,
|
||||
createDashboardViaApi,
|
||||
deleteDashboardViaApi,
|
||||
fetchDashboardData,
|
||||
gotoDashboardsList,
|
||||
renameDashboardViaToolbar,
|
||||
SEARCH_PLACEHOLDER,
|
||||
} from '../../../helpers/dashboards';
|
||||
|
||||
const TELEMETRY_DEPENDENT_VARS = ['q_env', 'q_service', 'd_namespace'];
|
||||
@@ -684,4 +688,38 @@ test.describe('Dashboard Detail — Configure drawer', () => {
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
// ─── Toolbar rename ──────────────────────────────────────────────────────
|
||||
// The toolbar options popover uses a separate PUT path from the Configure
|
||||
// drawer (TC-02 above). Both paths must persist server-side and surface in
|
||||
// the list view — this catches optimistic-update regressions specific to
|
||||
// the toolbar entry point.
|
||||
|
||||
test('TC-22 rename via toolbar options popover persists to the toolbar title', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const id = await seed(page, 'cfg-toolbar-rename');
|
||||
await page.goto(`/dashboard/${id}`);
|
||||
|
||||
// DashboardDescription toolbar always renders — even on blank dashboards.
|
||||
await expect(page.getByTestId('options')).toBeVisible();
|
||||
|
||||
await renameDashboardViaToolbar(page, 'cfg-toolbar-rename-renamed');
|
||||
|
||||
await expect(page.getByTestId('dashboard-title')).toHaveText(
|
||||
'cfg-toolbar-rename-renamed',
|
||||
);
|
||||
|
||||
const persisted = await fetchDashboardData(page, id);
|
||||
expect(persisted.title).toBe('cfg-toolbar-rename-renamed');
|
||||
|
||||
// List view reflects the rename after navigating back.
|
||||
await gotoDashboardsList(page);
|
||||
await page
|
||||
.getByPlaceholder(SEARCH_PLACEHOLDER)
|
||||
.fill('cfg-toolbar-rename-renamed');
|
||||
await expect(
|
||||
page.getByText('cfg-toolbar-rename-renamed').first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
createDashboardViaApi,
|
||||
createVariablesDashboardViaApi,
|
||||
deleteDashboardViaApi,
|
||||
openDashboardSettingsDrawer,
|
||||
} from '../../../helpers/dashboards';
|
||||
|
||||
const seedIds = new Set<string>();
|
||||
@@ -240,4 +241,25 @@ test.describe('Dashboard Detail Page — Edge Cases', () => {
|
||||
// document.title is set from the dashboard name — confirm it is intact.
|
||||
await expect(page).toHaveTitle(new RegExp('Spec & Chars'));
|
||||
});
|
||||
|
||||
test('TC-09 navigating away with the settings drawer open does not crash', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const id = await createDashboardViaApi(page, 'edge-drawer-nav-away');
|
||||
seedIds.add(id);
|
||||
await page.goto(`/dashboard/${id}`);
|
||||
|
||||
await openDashboardSettingsDrawer(page);
|
||||
|
||||
// Navigate away without closing the drawer.
|
||||
await page.goto('/dashboard');
|
||||
await expect(page).toHaveURL(/\/dashboard($|\?)/);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Dashboards', level: 1 }),
|
||||
).toBeVisible();
|
||||
// No error overlay should be present.
|
||||
await expect(
|
||||
page.getByRole('alert').filter({ hasText: /error/i }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import path from 'path';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from '../../fixtures/auth';
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
createDashboardViaApi,
|
||||
DEFAULT_DASHBOARD_TITLE,
|
||||
deleteDashboardViaApi,
|
||||
fetchDashboardData,
|
||||
findDashboardIdByTitle,
|
||||
gotoDashboardsList,
|
||||
importApmMetricsDashboardViaUI,
|
||||
@@ -15,6 +17,11 @@ import {
|
||||
SEARCH_PLACEHOLDER,
|
||||
} from '../../helpers/dashboards';
|
||||
|
||||
const APM_METRICS_TESTDATA_PATH = path.resolve(
|
||||
__dirname,
|
||||
'../../testdata/apm-metrics.json',
|
||||
);
|
||||
|
||||
// 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
|
||||
@@ -422,12 +429,140 @@ test.describe('Dashboards List Page', () => {
|
||||
await expect(page).toHaveURL(/\/dashboard($|\?)/);
|
||||
});
|
||||
|
||||
test('TC-19 import via file upload creates dashboard, navigates to detail, and surfaces metadata in list', 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')
|
||||
.filter({ hasText: 'Import Dashboard JSON' });
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
const postResponse = page.waitForResponse(
|
||||
(r) =>
|
||||
r.request().method() === 'POST' && /\/api\/v1\/dashboards/.test(r.url()),
|
||||
);
|
||||
await dialog
|
||||
.locator('input[type="file"]')
|
||||
.setInputFiles(APM_METRICS_TESTDATA_PATH);
|
||||
await dialog.getByRole('button', { name: 'Import and Next' }).click();
|
||||
const res = await postResponse;
|
||||
|
||||
expect(res.status()).toBeGreaterThanOrEqual(200);
|
||||
expect(res.status()).toBeLessThan(300);
|
||||
|
||||
await page.waitForURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
|
||||
// Register for cleanup.
|
||||
const urlMatch = page.url().match(/\/dashboard\/([0-9a-f-]+)/);
|
||||
expect(urlMatch, 'URL must contain dashboard ID').not.toBeNull();
|
||||
seedIds.add(urlMatch![1]);
|
||||
|
||||
await expect(page.getByTestId('dashboard-title')).toHaveText(
|
||||
APM_METRICS_TITLE,
|
||||
);
|
||||
|
||||
// Server-side check: every widget + tag from the fixture must be persisted.
|
||||
// A partial import (e.g. silently dropped widgets) would pass the UI title
|
||||
// check but fail here. The apm-metrics fixture has 16 widgets and 4 tags.
|
||||
const persisted = await fetchDashboardData(page, urlMatch![1]);
|
||||
expect(persisted.widgets?.length).toBe(16);
|
||||
expect(persisted.tags).toEqual(
|
||||
expect.arrayContaining(['apm', 'latency', 'error rate', 'throughput']),
|
||||
);
|
||||
|
||||
// Navigate back and confirm the imported dashboard surfaces in the list
|
||||
// with at least one tag chip.
|
||||
await gotoDashboardsList(page);
|
||||
await page.getByPlaceholder(SEARCH_PLACEHOLDER).fill(APM_METRICS_TITLE);
|
||||
await expect(page.getByText(APM_METRICS_TITLE).first()).toBeVisible();
|
||||
// The apm-metrics fixture has tags ['apm', 'latency', 'error rate', 'throughput'].
|
||||
await expect(page.getByText('apm').first()).toBeVisible();
|
||||
});
|
||||
|
||||
// The Monaco paste path is intentionally not covered — the file-upload
|
||||
// path (TC-19) exercises the same populate-editor-then-import code path.
|
||||
// Keyboard-typing large JSON into Monaco is unreliable in headless CI.
|
||||
|
||||
test('TC-20 invalid JSON via file upload shows "Invalid JSON" error', 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')
|
||||
.filter({ hasText: 'Import Dashboard JSON' });
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Track POST attempts: invalid JSON must never reach the create endpoint.
|
||||
let postFired = false;
|
||||
await page.route(/\/api\/v1\/dashboards(\?|$)/, (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
postFired = true;
|
||||
}
|
||||
void route.continue();
|
||||
});
|
||||
|
||||
await dialog.locator('input[type="file"]').setInputFiles({
|
||||
name: 'bad.json',
|
||||
mimeType: 'application/json',
|
||||
buffer: Buffer.from('not valid json {'),
|
||||
});
|
||||
|
||||
await expect(dialog.getByText('Invalid JSON')).toBeVisible();
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Clicking "Import and Next" with invalid content should surface an error
|
||||
// and keep the dialog open.
|
||||
await dialog.getByRole('button', { name: 'Import and Next' }).click();
|
||||
await expect(page).not.toHaveURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
expect(postFired, 'invalid JSON must not trigger POST').toBe(false);
|
||||
});
|
||||
|
||||
test('TC-21 import with empty editor clicking Import and Next shows error', 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')
|
||||
.filter({ hasText: 'Import Dashboard JSON' });
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
let postFired = false;
|
||||
await page.route(/\/api\/v1\/dashboards(\?|$)/, (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
postFired = true;
|
||||
}
|
||||
void route.continue();
|
||||
});
|
||||
|
||||
await dialog.getByRole('button', { name: 'Import and Next' }).click();
|
||||
|
||||
await expect(dialog.getByText('Error loading JSON file')).toBeVisible();
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(page).not.toHaveURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
expect(postFired, 'empty editor must not trigger POST').toBe(false);
|
||||
});
|
||||
|
||||
// ─── 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 ({
|
||||
test('TC-22 delete confirmation dialog shows dashboard name with Cancel and Delete buttons', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-delete-confirm';
|
||||
@@ -456,7 +591,7 @@ test.describe('Dashboards List Page', () => {
|
||||
await expect(dialog.getByRole('button', { name: 'Delete' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-20 cancelling delete navigates to the dashboard detail page (known behaviour)', async ({
|
||||
test('TC-23 cancelling delete navigates to the dashboard detail page (known behaviour)', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-delete-cancel';
|
||||
@@ -471,7 +606,7 @@ test.describe('Dashboards List Page', () => {
|
||||
await expect(page).toHaveURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
});
|
||||
|
||||
test('TC-21 confirming delete removes the dashboard from the list', async ({
|
||||
test('TC-24 confirming delete removes the dashboard from the list', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-delete-confirmed';
|
||||
@@ -506,7 +641,7 @@ test.describe('Dashboards List Page', () => {
|
||||
|
||||
// ─── Row click navigation ────────────────────────────────────────────────
|
||||
|
||||
test('TC-22 clicking a dashboard row navigates to the detail page', async ({
|
||||
test('TC-25 clicking a dashboard row navigates to the detail page', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-row-click';
|
||||
@@ -520,7 +655,7 @@ test.describe('Dashboards List Page', () => {
|
||||
await expect(page).toHaveURL(/\/dashboard\/[0-9a-f-]+/);
|
||||
});
|
||||
|
||||
test('TC-23 sidebar Dashboards link navigates to the list page', async ({
|
||||
test('TC-26 sidebar Dashboards link navigates to the list page', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await page.goto('/home');
|
||||
@@ -538,7 +673,7 @@ test.describe('Dashboards List Page', () => {
|
||||
|
||||
// ─── URL state and deep linking ──────────────────────────────────────────
|
||||
|
||||
test('TC-24 browser Back after navigating to a dashboard restores search state', async ({
|
||||
test('TC-27 browser Back after navigating to a dashboard restores search state', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
const name = 'dashboards-list-back-search';
|
||||
@@ -557,7 +692,7 @@ test.describe('Dashboards List Page', () => {
|
||||
await expect(page.getByPlaceholder(SEARCH_PLACEHOLDER)).toHaveValue(name);
|
||||
});
|
||||
|
||||
test('TC-25 direct navigation with sort params honours them on load', async ({
|
||||
test('TC-28 direct navigation with sort params honours them on load', async ({
|
||||
authedPage: page,
|
||||
}) => {
|
||||
await page.goto('/dashboard?columnKey=updatedAt&order=descend');
|
||||
|
||||
Reference in New Issue
Block a user