Dashboards: User journey E2Es (#109049)

* wip

* wip

* wip

* wip

* scope e2es

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* add dashboard view tests

* mods cujs

* wip refactor

* remove timeouts

* fixes

* betterer

* fixes

* refactor

* refactor

* fix

* use type instead of any

* betterer

* PR mods + codeowners

* CODEOWNERS

* readme lint
This commit is contained in:
Victor Marin 2025-09-04 15:17:54 +03:00 committed by GitHub
parent 4d818292d8
commit 27b3137baf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1884 additions and 0 deletions

2
.github/CODEOWNERS vendored
View File

@ -410,7 +410,9 @@
/e2e/ @grafana/grafana-frontend-platform /e2e/ @grafana/grafana-frontend-platform
/e2e-playwright/cloud-plugins-suite/ @grafana/partner-datasources /e2e-playwright/cloud-plugins-suite/ @grafana/partner-datasources
/e2e-playwright/dashboard-new-layouts/ @grafana/dashboards-squad /e2e-playwright/dashboard-new-layouts/ @grafana/dashboards-squad
/e2e-playwright/dashboard-cujs/ @grafana/dashboards-squad
/e2e-playwright/dashboards-search-suite/ @grafana/dashboards-squad /e2e-playwright/dashboards-search-suite/ @grafana/dashboards-squad
/e2e-playwright/dashboards/cujs/ @grafana/dashboards-squad
/e2e-playwright/dashboards/DashboardLiveTest.json @grafana/dashboards-squad /e2e-playwright/dashboards/DashboardLiveTest.json @grafana/dashboards-squad
/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json @grafana/dashboards-squad /e2e-playwright/dashboards/DataLinkWithoutSlugTest.json @grafana/dashboards-squad
/e2e-playwright/dashboards/PanelSandboxDashboard.json @grafana/plugins-platform-frontend /e2e-playwright/dashboards/PanelSandboxDashboard.json @grafana/plugins-platform-frontend

View File

@ -0,0 +1,39 @@
# Dashboard Critical User Journeys (CUJs) E2E Tests
This directory contains end-to-end tests for critical user journeys related to dashboard functionality in Grafana, specifically testing AdHoc Filters, GroupBy Variables, Scopes, and Dashboard Navigation. The test suite validates core dashboard workflows including filtering data across dashboards, autocomplete functionality, operator selection, grouping across multiple dimensions, scope selection and persistence, and navigation between dashboards with state management. Tests use three pre-configured dashboard JSON files (cuj-dashboard-1, cuj-dashboard-2, cuj-dashboard-3) that contain text panels displaying variable values, AdHoc filter variables, GroupBy variables, and time range controls connected to a Prometheus datasource.
## Environment Flags
### `NO_DASHBOARD_IMPORT`
**Default**: `false`
When set to `true`, skips importing test dashboards during global setup. Use this when running against a live Grafana instance that already has the required dashboards with matching UIDs (`cuj-dashboard-1`, `cuj-dashboard-2`, `cuj-dashboard-3`).
```bash
NO_DASHBOARD_IMPORT=true yarn e2e:playwright
```
### `API_CONFIG_PATH`
**Default**: `../dashboards/cujs/config.json`
Configures the path to the API mocking configuration file. This enables dynamic API endpoint configuration for different environments.
If the `API_CONFIG_PATH` is not set, the test suite will use mocked responses for API calls using the default configuration, which has settings for the default testing environment. If set, the test suite will make real API calls instead of using mocks, based on the configuration provided in the specified file. This configuration would be used only in live data scenarios where the endpoints might differ due to testing on different dashboards that might use different DataSources which furthermore might have different API endpoints.
The config file should contain endpoint glob patterns for labels and values APIs. These endpoints are used to fetch labels (keys) and values for the AdHocFilters and the GroupBy variables. The pattern should be a string glob pattern, e.g.: '\*\*/resources/\*\*/labels\*'
```bash
API_CONFIG_PATH=/path/to/custom-config.json yarn e2e:playwright
```
## Global Setup and Teardown
### Setup Process (`global-setup.spec.ts`)
The global setup imports three CUJ dashboard JSON files unless `NO_DASHBOARD_IMPORT` is true. It posts each dashboard to `/api/dashboards/import` with overwrite enabled, stores the returned dashboard UIDs in `process.env.DASHBOARD_UIDS` via `setDashboardUIDs()`, and handles cases where dashboards already exist by overwriting them.
### Teardown Process (`global-teardown.spec.ts`)
The teardown retrieves stored dashboard UIDs using `getDashboardUIDs()`, deletes each dashboard via `DELETE /api/dashboards/uid/{uid}`, and cleans up the environment state with `clearDashboardUIDs()`. The dashboard UID state management is handled through `dashboardUidsState.ts` which provides functions to store, retrieve, and clear UIDs in the process environment.

View File

@ -0,0 +1,241 @@
import { test, expect } from '@grafana/plugin-e2e';
import { setScopes } from '../utils/scope-helpers';
import {
getAdHocFilterOptionValues,
getAdHocFilterPills,
getAdHocFilterRestoreButton,
getAdhocFiltersInput,
getMarkdownHTMLContent,
getScopesSelectorInput,
waitForAdHocOption,
} from './cuj-selectors';
import { prepareAPIMocks } from './utils';
export const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
test.use({
featureToggles: {
scopeFilters: true,
groupByVariable: true,
reloadDashboardsOnParamsChange: true,
},
});
test.describe(
'AdHoc Filters CUJs',
{
tag: ['@dashboard-cujs'],
},
() => {
test('Filter data on a dashboard', async ({ page, selectors, gotoDashboardPage }) => {
const apiMocks = await prepareAPIMocks(page);
const adHocFilterPills = getAdHocFilterPills(page);
const scopesSelectorInput = getScopesSelectorInput(page);
await test.step('1.Apply filtering to a whole dashboard', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
await expect(adHocFilterPills.first()).toBeVisible();
expect(await adHocFilterPills.count()).toBe(2);
const adHocVariable = dashboardPage
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('adHoc'))
.locator('..')
.locator('input');
const labelsResponsePromise = page.waitForResponse(apiMocks.labels);
await adHocVariable.click();
await labelsResponsePromise;
await adHocVariable.press('Enter');
await waitForAdHocOption(page);
const valuesResponsePromise = page.waitForResponse(apiMocks.values);
await adHocVariable.press('Enter');
await valuesResponsePromise;
await waitForAdHocOption(page);
await adHocVariable.press('Enter');
expect(await adHocFilterPills.count()).toBe(3);
const pills = await adHocFilterPills.allTextContents();
const processedPills = pills
.map((p) => {
const parts = p.split(' ');
return `${parts[0]}${parts[1]}"${parts[2]}"`;
})
.join(',');
// assert the panel is visible and has the correct value
const markdownContent = await getMarkdownHTMLContent(dashboardPage, selectors);
await expect(markdownContent).toContainText(`AdHocVar: ${processedPills}`);
});
await test.step('2.Autocomplete for the filter values', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const adHocVariable = getAdhocFiltersInput(dashboardPage, selectors);
const labelsResponsePromise = page.waitForResponse(apiMocks.labels);
await adHocVariable.click();
await labelsResponsePromise;
await adHocVariable.press('Enter');
await waitForAdHocOption(page);
const valuesResponsePromise = page.waitForResponse(apiMocks.values);
await adHocVariable.press('Enter');
await valuesResponsePromise;
const valuesLocator = getAdHocFilterOptionValues(page);
const valuesCount = await valuesLocator.count();
const firstValue = await valuesLocator.first().textContent();
await adHocVariable.fill(firstValue!.slice(0, -1));
await waitForAdHocOption(page);
const newValuesCount = await valuesLocator.count();
expect(newValuesCount).toBeLessThan(valuesCount);
//exclude the custom value
expect(newValuesCount).toBeGreaterThan(1);
});
await test.step('3.Choose operators on the filters', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
await expect(adHocFilterPills.first()).toBeVisible();
expect(await adHocFilterPills.count()).toBe(2);
const adHocVariable = getAdhocFiltersInput(dashboardPage, selectors);
const labelsResponsePromise = page.waitForResponse(apiMocks.labels);
await adHocVariable.click();
await labelsResponsePromise;
await adHocVariable.press('Enter');
await waitForAdHocOption(page);
await adHocVariable.press('ArrowDown');
await adHocVariable.press('ArrowDown');
await adHocVariable.press('ArrowDown');
await adHocVariable.press('ArrowDown');
const valuesResponsePromise = page.waitForResponse(apiMocks.values);
await adHocVariable.press('Enter');
await valuesResponsePromise;
await adHocVariable.press('Enter');
expect(await adHocFilterPills.count()).toBe(3);
const pills = await adHocFilterPills.allTextContents();
const processedPills = pills
.map((p) => {
const parts = p.split(' ');
return `${parts[0]}${parts[1]}"${parts[2]}"`;
})
.join(',');
// assert the panel is visible and has the correct value
const markdownContent = await getMarkdownHTMLContent(dashboardPage, selectors);
await expect(markdownContent).toContainText(`AdHocVar: ${processedPills}`);
// regex operator applied to the filter
await expect(markdownContent).toContainText(`=~`);
});
await test.step('4.Edit and restore default filters applied to the dashboard', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const defaultDashboardFilter = adHocFilterPills.first();
const pillText = await defaultDashboardFilter.textContent();
const adHocVariable = getAdhocFiltersInput(dashboardPage, selectors).first();
await defaultDashboardFilter.click();
await adHocVariable.fill('new value');
await adHocVariable.press('Enter');
expect(await defaultDashboardFilter.textContent()).not.toBe(pillText);
const restoreButton = getAdHocFilterRestoreButton(page, 'dashboard');
await restoreButton.click();
expect(await defaultDashboardFilter.textContent()).toBe(pillText);
});
await test.step('5.Edit and restore filters implied by scope', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
await expect(adHocFilterPills.first()).toBeVisible();
expect(await adHocFilterPills.count()).toBe(2);
await setScopes(page);
await expect(scopesSelectorInput).toHaveValue(/.+/);
expect(await adHocFilterPills.count()).toBe(3);
const defaultDashboardFilter = adHocFilterPills.first();
const pillText = await defaultDashboardFilter.textContent();
const adHocVariable = getAdhocFiltersInput(dashboardPage, selectors).first();
await defaultDashboardFilter.click();
await adHocVariable.fill('new value');
await adHocVariable.press('Enter');
expect(await defaultDashboardFilter.textContent()).not.toBe(pillText);
const restoreButton = getAdHocFilterRestoreButton(page, 'scope');
await restoreButton.click();
expect(await defaultDashboardFilter.textContent()).toBe(pillText);
});
await test.step('6.Add and edit filters through keyboard', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
await expect(adHocFilterPills.first()).toBeVisible();
expect(await adHocFilterPills.count()).toBe(2);
const adHocVariable = getAdhocFiltersInput(dashboardPage, selectors);
const labelsResponsePromise = page.waitForResponse(apiMocks.labels);
await adHocVariable.click();
await labelsResponsePromise;
await adHocVariable.press('Enter');
await waitForAdHocOption(page);
const valuesResponsePromise = page.waitForResponse(apiMocks.values);
await adHocVariable.press('Enter');
await valuesResponsePromise;
const secondLabelsPromise = page.waitForResponse(apiMocks.labels);
await adHocVariable.press('Enter');
// add another filter
await secondLabelsPromise;
await adHocVariable.press('ArrowDown');
await adHocVariable.press('Enter');
// arrow down to multivalue op
await adHocVariable.press('ArrowDown');
await adHocVariable.press('ArrowDown');
await adHocVariable.press('ArrowDown');
const secondValuesResponsePromise = page.waitForResponse(apiMocks.values);
await adHocVariable.press('Enter');
await secondValuesResponsePromise;
//select firs value, then arrow down to another
await adHocVariable.press('Enter');
await adHocVariable.press('ArrowDown');
await adHocVariable.press('Enter');
//escape applies it
await adHocVariable.press('Escape');
expect(await adHocFilterPills.count()).toBe(4);
//remove last value through keyboard
await page.keyboard.press('Shift+Tab');
await page.keyboard.press('Enter');
expect(await adHocFilterPills.count()).toBe(3);
});
});
}
);

View File

@ -0,0 +1,82 @@
import { Page } from '@playwright/test';
import { expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
export function getAdHocFilterPills(page: Page) {
return page.getByLabel(/^Edit filter with key/);
}
export async function waitForAdHocOption(page: Page) {
await page.waitForSelector('[role="option"]', { state: 'visible' });
}
export async function getMarkdownHTMLContent(page: DashboardPage, selectors: E2ESelectorGroups) {
const panelContent = page.getByGrafanaSelector(selectors.components.Panels.Panel.content).first();
await expect(panelContent).toBeVisible();
return panelContent.locator('.markdown-html');
}
export function getAdhocFiltersInput(page: DashboardPage, selectors: E2ESelectorGroups) {
return page
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('adHoc'))
.locator('..')
.locator('input');
}
export function getGroupByInput(page: DashboardPage, selectors: E2ESelectorGroups) {
return page
.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('groupBy'))
.locator('..')
.locator('input');
}
export function getAdHocFilterOptionValues(page: Page) {
return page.getByTestId(/^data-testid ad hoc filter option value/);
}
export function getAdHocFilterRestoreButton(page: Page, type: string) {
if (type === 'dashboard') {
return page.getByLabel('Restore the value set by this dashboard.');
}
if (type === 'scope') {
return page.getByLabel('Restore the value set by your selected scope.');
}
return page.getByLabel('Restore filter to its original value.');
}
export function getGroupByRestoreButton(page: Page) {
return page.getByLabel('Restore groupby set by this dashboard.');
}
export function getScopesSelectorInput(page: Page) {
return page.getByTestId('scopes-selector-input');
}
export function getRecentScopesSelector(page: Page) {
return page.getByTestId('scopes-selector-recent-scopes-section');
}
export function getScopeTreeCheckboxes(page: Page) {
return page.locator('input[type="checkbox"][data-testid^="scopes-tree"]');
}
export function getScopesDashboards(page: Page) {
return page.locator('[data-testid^="scopes-dashboards-"][role="treeitem"]');
}
export function getScopesDashboardsSearchInput(page: Page) {
return page.getByTestId('scopes-dashboards-search');
}
export function getGroupByValues(page: Page) {
return page
.getByTestId(/^GroupBySelect-/)
.first()
.locator('div:has(+ button)');
}
export function getGroupByOptions(page: Page) {
return page.getByTestId('data-testid Select option');
}

View File

@ -0,0 +1,156 @@
import { test, expect } from '@grafana/plugin-e2e';
import { setScopes } from '../utils/scope-helpers';
import {
getAdHocFilterPills,
getGroupByInput,
getGroupByValues,
getMarkdownHTMLContent,
getScopesDashboards,
getScopesDashboardsSearchInput,
getScopesSelectorInput,
} from './cuj-selectors';
test.use({
featureToggles: {
scopeFilters: true,
groupByVariable: true,
reloadDashboardsOnParamsChange: true,
},
});
export const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
export const NAVIGATE_TO = 'cuj-dashboard-2';
test.describe(
'Dashboard navigation CUJs',
{
tag: ['@dashboard-cujs'],
},
() => {
test('Navigate between dashboards', async ({ page, gotoDashboardPage, selectors }) => {
const scopeSelectorInput = getScopesSelectorInput(page);
const scopesDashboards = getScopesDashboards(page);
const scopesDashboardsSearchInput = getScopesDashboardsSearchInput(page);
const adhocFilterPills = getAdHocFilterPills(page);
const groupByValues = getGroupByValues(page);
await test.step('1.Search dashboard', async () => {
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
await setScopes(page);
await expect(scopeSelectorInput).toHaveValue(/.+/);
const firstDbName = await scopesDashboards.first().textContent();
const scopeDashboardsCount = await scopesDashboards.count();
expect(scopeDashboardsCount).toBeGreaterThan(0);
await scopesDashboardsSearchInput.fill(firstDbName!.trim().slice(0, 5));
await expect(scopesDashboards).not.toHaveCount(scopeDashboardsCount);
});
await test.step('2.Time selection persisting', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
await setScopes(page);
await expect(scopeSelectorInput).toHaveValue(/.+/);
// assert the panel is visible and has the correct value
const markdownContent = await getMarkdownHTMLContent(dashboardPage, selectors);
await expect(markdownContent).toContainText(`now-6h`);
const timePickerButton = page.getByTestId(selectors.components.TimePicker.openButton);
await timePickerButton.click();
const label = page.getByText('Last 12 hours');
await label.click();
await expect(markdownContent).toContainText(`now-12h`);
await scopesDashboards.first().click();
await page.waitForURL('**/d/**');
await expect(markdownContent).toBeVisible();
await expect(markdownContent).toContainText(`now-12h`);
});
await test.step('3.See filter/groupby selection persisting when navigating from dashboard to dashboard', async () => {
const dashboardPage = await gotoDashboardPage({ uid: NAVIGATE_TO });
await setScopes(page, { title: 'CUJ Dashboard 3', uid: 'cuj-dashboard-3' });
await expect(scopeSelectorInput).toHaveValue(/.+/);
const pills = await adhocFilterPills.allTextContents();
const processedPills = pills
.map((p) => {
const parts = p.split(' ');
return `${parts[0]}${parts[1]}"${parts[2]}"`;
})
.join(',');
// assert the panel is visible and has the correct value
const markdownContent = await getMarkdownHTMLContent(dashboardPage, selectors);
// no groupBy value
await expect(markdownContent).toContainText(`GroupByVar: \n\nAdHocVar: ${processedPills}`);
const groupByVariable = getGroupByInput(dashboardPage, selectors);
// add a custom groupBy value
await groupByVariable.click();
await groupByVariable.fill('dev');
await groupByVariable.press('Enter');
await groupByVariable.press('Escape');
await expect(scopesDashboards.first()).toBeVisible();
await scopesDashboards.first().click();
await page.waitForURL('**/d/**');
//all values are set after dashboard switch
await expect(markdownContent).toContainText(`GroupByVar: dev\n\nAdHocVar: ${processedPills}`);
});
await test.step('4.Unmodified default filters and groupBy keys are not propagated to a different dashboard', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
await setScopes(page, { title: 'CUJ Dashboard 2', uid: 'cuj-dashboard-2' });
await expect(scopeSelectorInput).toHaveValue(/.+/);
const pillCount = await adhocFilterPills.count();
const pillTexts = await adhocFilterPills.allTextContents();
const processedPills = pillTexts
.map((p) => {
const parts = p.split(' ');
return `${parts[0]}${parts[1]}"${parts[2]}"`;
})
.join(',');
const groupByCount = await groupByValues.count();
const selectedValues = (await groupByValues.allTextContents()).join(', ');
// assert the panel is visible and has the correct value
const markdownContent = await getMarkdownHTMLContent(dashboardPage, selectors);
const oldFilters = `GroupByVar: ${selectedValues}\n\nAdHocVar: ${processedPills}`;
await expect(markdownContent).toContainText(oldFilters);
await expect(scopesDashboards.first()).toBeVisible();
await scopesDashboards.first().click();
await page.waitForURL('**/d/**');
const newPillCount = await adhocFilterPills.count();
const newGroupByCount = await groupByValues.count();
expect(newPillCount).not.toEqual(pillCount);
expect(newGroupByCount).not.toEqual(groupByCount);
expect(newGroupByCount).toBe(0);
});
});
}
);

View File

@ -0,0 +1,178 @@
import { test, expect } from '@grafana/plugin-e2e';
test.use({
featureToggles: {
scopeFilters: true,
groupByVariable: true,
reloadDashboardsOnParamsChange: true,
},
});
export const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
export const PANEL_UNDER_TEST = 'Panel Title';
test.describe(
'Dashboard view CUJs',
{
tag: ['@dashboard-cujs'],
},
() => {
test('View a dashboard', async ({ page, gotoDashboardPage, selectors }) => {
await test.step('1.Top level selectors', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const groupByVariable = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels('groupBy')
);
const adHocVariable = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.SubMenu.submenuItemLabels('adHoc')
);
expect(groupByVariable).toBeVisible();
expect(adHocVariable).toBeVisible();
});
await test.step('2.View individual panel', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const viewPanelBreadcrumb = dashboardPage.getByGrafanaSelector(
selectors.components.Breadcrumbs.breadcrumb('View panel')
);
await expect(viewPanelBreadcrumb).not.toBeVisible();
const panelTitle = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Panel.title(PANEL_UNDER_TEST)
);
await expect(panelTitle).toBeVisible();
// Open panel menu and click edit
await panelTitle.hover();
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)).click();
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('View')).click();
await expect(viewPanelBreadcrumb).toBeVisible();
await dashboardPage
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
.click();
await expect(viewPanelBreadcrumb).not.toBeVisible();
});
await test.step('3.Set time range for the dashboard', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton);
await test.step('3.1.Click on Quick range', async () => {
await page.mouse.move(0, 0);
await expect.soft(timePickerButton).toContainText('Last 6 hours');
await timePickerButton.click();
const label = page.getByText('Last 5 minutes');
await label.click();
expect.soft(await timePickerButton.textContent()).toContain('Last 5 minutes');
});
await test.step('3.2.Set absolute time range', async () => {
await timePickerButton.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.TimePicker.fromField)
.fill('2024-01-01 00:00:00');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField).fill('2024-01-01 23:59:59');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.applyTimeRange).click();
expect.soft(await timePickerButton.textContent()).toContain('2024-01-01 00:00:00 to 2024-01-01 23:59:59');
});
await test.step('3.3.Change time zone', async () => {
await timePickerButton.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.TimeZonePicker.changeTimeSettingsButton)
.click();
const timeZoneSelectionArea = page.locator('section[aria-label="Time zone selection"]');
expect(timeZoneSelectionArea).toBeVisible();
expect(await timeZoneSelectionArea.textContent()).toContain('Browser Time');
await dashboardPage.getByGrafanaSelector(selectors.components.TimeZonePicker.containerV2).click();
const label = page.getByText('Coordinated Universal Time');
await label.click();
expect(await timeZoneSelectionArea.textContent()).toContain('Coordinated Universal Time');
await timePickerButton.click();
});
await test.step('3.4.Navigate time range', async () => {
await timePickerButton.click();
await dashboardPage
.getByGrafanaSelector(selectors.components.TimePicker.fromField)
.fill('2024-01-01 08:30:00');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField).fill('2024-01-01 08:40:00');
await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.applyTimeRange).click();
expect.soft(await timePickerButton.textContent()).toContain('2024-01-01 08:30:00 to 2024-01-01 08:40:00');
const forwardBtn = page.locator('button[aria-label="Move time range forwards"]');
const backwardBtn = page.locator('button[aria-label="Move time range backwards"]');
await forwardBtn.click();
expect.soft(await timePickerButton.textContent()).toContain('2024-01-01 08:35:00 to 2024-01-01 08:45:00');
await backwardBtn.click();
await backwardBtn.click();
expect.soft(await timePickerButton.textContent()).toContain('2024-01-01 08:25:00 to 2024-01-01 08:35:00');
});
});
await test.step('4.Force refresh', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const refreshBtn = dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2);
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).nth(1);
const panelContents = await panelContent.textContent();
await refreshBtn.click();
await page.waitForLoadState('networkidle');
expect(await panelContent.textContent()).not.toBe(panelContents);
});
await test.step('5.Turn off refresh', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const intervalRefreshBtn = dashboardPage.getByGrafanaSelector(
selectors.components.RefreshPicker.intervalButtonV2
);
await intervalRefreshBtn.click();
const btn = page.locator('button[aria-label="5 seconds"]');
await btn.click();
const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).nth(1);
const initialPanelContents = await panelContent.textContent();
await expect(panelContent).not.toHaveText(initialPanelContents!, {
timeout: 7000,
});
const refreshedPanelContents = await panelContent.textContent();
await intervalRefreshBtn.click();
const offBtn = page.locator('button[aria-label="Turn off auto refresh"]');
await offBtn.click();
await expect(panelContent).toHaveText(refreshedPanelContents!, {
timeout: 7000,
});
});
});
}
);

View File

@ -0,0 +1,11 @@
export const setDashboardUIDs = (uids: string[]) => {
process.env.DASHBOARD_UIDS = JSON.stringify(uids);
};
export const getDashboardUIDs = (): string[] => {
return JSON.parse(process.env.DASHBOARD_UIDS || '[]');
};
export const clearDashboardUIDs = () => {
delete process.env.DASHBOARD_UIDS;
};

View File

@ -0,0 +1,40 @@
import { test } from '@playwright/test';
import cujDashboardOne from '../dashboards/cujs/cuj-dashboard-1.json';
import cujDashboardTwo from '../dashboards/cujs/cuj-dashboard-2.json';
import cujDashboardThree from '../dashboards/cujs/cuj-dashboard-3.json';
import { setDashboardUIDs } from './dashboardUidsState';
const dashboards = [cujDashboardOne, cujDashboardTwo, cujDashboardThree];
// should be used with live instances that already have dashboards
// that match the test dashboard UIDs and format
const NO_DASHBOARD_IMPORT = Boolean(process.env.NO_DASHBOARD_IMPORT);
test.describe('Dashboard CUJS Global Setup', () => {
test('import test dashboards', async ({ request }) => {
const dashboardUIDs: string[] = [];
if (NO_DASHBOARD_IMPORT) {
return;
}
// Import all test dashboards
for (const dashboard of dashboards) {
const response = await request.post('/api/dashboards/import', {
data: {
dashboard,
folderUid: '',
overwrite: true,
inputs: [],
},
});
const responseBody = await response.json();
dashboardUIDs.push(responseBody.uid);
}
setDashboardUIDs(dashboardUIDs);
});
});

View File

@ -0,0 +1,19 @@
import { test } from '@playwright/test';
import { clearDashboardUIDs, getDashboardUIDs } from './dashboardUidsState';
test.describe('Dashboard CUJS Global Teardown', () => {
test('cleanup test dashboards', async ({ request }) => {
const dashboardUIDs = getDashboardUIDs();
if (!dashboardUIDs) {
return;
}
for (const dashboardUID of dashboardUIDs) {
await request.delete(`/api/dashboards/uid/${dashboardUID}`);
}
clearDashboardUIDs();
});
});

View File

@ -0,0 +1,117 @@
import { test, expect } from '@grafana/plugin-e2e';
import {
getGroupByInput,
getGroupByOptions,
getGroupByRestoreButton,
getGroupByValues,
getMarkdownHTMLContent,
} from './cuj-selectors';
import { prepareAPIMocks } from './utils';
test.use({
featureToggles: {
scopeFilters: true,
groupByVariable: true,
reloadDashboardsOnParamsChange: true,
},
});
export const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
test.describe(
'GroupBy CUJs',
{
tag: ['@dashboard-cujs'],
},
() => {
test('Groupby data on a dashboard', async ({ page, selectors, gotoDashboardPage }) => {
prepareAPIMocks(page);
const groupByOptions = getGroupByOptions(page);
const groupByValues = getGroupByValues(page);
const groupByRestoreButton = getGroupByRestoreButton(page);
await test.step('1.Apply a groupBy across one or mulitple dimensions', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const groupByVariable = getGroupByInput(dashboardPage, selectors);
await groupByVariable.click();
const groupByOption = groupByOptions.nth(1);
await groupByOption.click();
await page.keyboard.press('Escape');
const selectedValues = await groupByValues.allTextContents();
// assert the panel is visible and has the correct value
const markdownContent = await getMarkdownHTMLContent(dashboardPage, selectors);
await expect(markdownContent).toContainText(`GroupByVar: ${selectedValues.join(', ')}`);
});
await test.step('2.Autocomplete for the groupby values', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const groupByVariable = getGroupByInput(dashboardPage, selectors);
await expect(groupByVariable).toBeVisible();
await groupByVariable.click();
const groupByOption = groupByOptions.nth(0);
const text = await groupByOption.textContent();
const optionsCount = await groupByOptions.count();
await groupByVariable.fill(text!);
const searchedOptionsCount = await groupByOptions.count();
expect(searchedOptionsCount).toBeLessThanOrEqual(optionsCount);
});
await test.step('3.Edit and restore default groupBy', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const initialSelectedOptionsCount = await groupByValues.count();
const groupByVariable = getGroupByInput(dashboardPage, selectors);
await groupByVariable.click();
const groupByOption = groupByOptions.nth(1);
await groupByOption.click();
await page.keyboard.press('Escape');
const afterEditOptionsCount = await groupByValues.count();
expect(afterEditOptionsCount).toBe(initialSelectedOptionsCount + 1);
await groupByRestoreButton.click();
await expect(groupByValues).not.toHaveCount(afterEditOptionsCount);
await expect(groupByValues).toHaveCount(initialSelectedOptionsCount);
});
await test.step('4.Enter multiple values using keyboard only', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const groupByVariable = getGroupByInput(dashboardPage, selectors);
await groupByVariable.click();
const groupByOptionOne = groupByOptions.nth(0);
const groupByOptionTwo = groupByOptions.nth(1);
const textOne = await groupByOptionOne.textContent();
const textTwo = await groupByOptionTwo.textContent();
await groupByVariable.fill(textOne!);
await page.keyboard.press('Enter');
await groupByVariable.fill(textTwo!);
await page.keyboard.press('Enter');
await page.keyboard.press('Escape');
await expect(page.getByText(textOne!, { exact: false }).first()).toBeVisible();
await expect(page.getByText(textTwo!, { exact: false }).first()).toBeVisible();
});
});
}
);

View File

@ -0,0 +1,181 @@
import { test, expect } from '@grafana/plugin-e2e';
import {
applyScopes,
expandScopesSelection,
getScopeLeafName,
getScopeLeafTitle,
getScopeTreeName,
openScopesSelector,
searchScopes,
selectScope,
TestScope,
} from '../utils/scope-helpers';
import { testScopes } from '../utils/scopes';
import { getRecentScopesSelector, getScopesSelectorInput, getScopeTreeCheckboxes } from './cuj-selectors';
test.use({
featureToggles: {
scopeFilters: true,
groupByVariable: true,
reloadDashboardsOnParamsChange: true,
},
});
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
export const DASHBOARD_UNDER_TEST = 'cuj-dashboard-1';
test.describe(
'Scope CUJs',
{
tag: ['@dashboard-cujs'],
},
() => {
test('Choose a scope', async ({ page, gotoDashboardPage }) => {
const scopesSelector = getScopesSelectorInput(page);
const recentScopesSelector = getRecentScopesSelector(page);
const scopeTreeCheckboxes = getScopeTreeCheckboxes(page);
await test.step('1.View and select any scope', async () => {
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
expect.soft(scopesSelector).toHaveValue('');
const scopes = testScopes();
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
let scopeName = await getScopeTreeName(page, 0);
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
scopeName = await getScopeTreeName(page, 1);
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
scopeName = await getScopeLeafName(page, 0);
let scopeTitle = await getScopeLeafTitle(page, 0);
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
expect.soft(scopesSelector).toHaveValue(scopeTitle);
});
await test.step('2.Select a scope across multiple types of production entities', async () => {
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
expect.soft(scopesSelector).toHaveValue('');
const scopes = testScopes();
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
let scopeName = await getScopeTreeName(page, 0);
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
scopeName = await getScopeTreeName(page, 1);
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
const scopeTitles: string[] = [];
const selectedScopes = [secondLevelScopes[0], secondLevelScopes[1]]; //used only in mocked scopes version
for (let i = 0; i < selectedScopes.length; i++) {
scopeName = await getScopeLeafName(page, i);
scopeTitles.push(await getScopeLeafTitle(page, i));
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[i]); //used only in mocked scopes version
}
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
await expect.soft(scopesSelector).toHaveValue(scopeTitles.join(' + '));
});
await test.step('3.View and select a recently viewed scope', async () => {
// this step depends on the previous ones because they set recent scopes
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
expect.soft(scopesSelector).toHaveValue('');
const scopes = testScopes();
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
await recentScopesSelector.click();
const recentScope = recentScopesSelector.locator('../..').locator('button').nth(1);
const scopeName = await recentScope.locator('span').first().textContent();
await recentScope.click();
await expect.soft(scopesSelector).toHaveValue(scopeName!.replace(', ', ' + '));
});
await test.step('4.View and select a scope configured by any team', async () => {
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
expect.soft(scopesSelector).toHaveValue('');
const scopes = testScopes();
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes);
let scopeName = await getScopeTreeName(page, 1);
const firstLevelScopes = scopes[2].children!; //used only in mocked scopes version
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
scopeName = await getScopeTreeName(page, 1);
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
scopeName = await getScopeLeafName(page, 0);
let scopeTitle = await getScopeLeafTitle(page, 0);
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
await applyScopes(page, USE_LIVE_DATA ? undefined : []); //used only in mocked scopes version
expect.soft(scopesSelector).toHaveValue(new RegExp(`^${scopeTitle}`));
});
await test.step('5.View pre-completed production entity values as I type', async () => {
await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
const scopes = testScopes();
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
let scopeName = await getScopeTreeName(page, 0);
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
scopeName = await getScopeTreeName(page, 1);
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
const scopeSearchOne = await getScopeLeafTitle(page, 0);
const scopeSearchTwo = await getScopeLeafTitle(page, 1);
await searchScopes(page, scopeSearchOne, [secondLevelScopes[0]]);
await expect.soft(scopeTreeCheckboxes).toHaveCount(1);
expect.soft(await scopeTreeCheckboxes.first().locator('../..').textContent()).toBe(scopeSearchOne);
await searchScopes(page, scopeSearchTwo, [secondLevelScopes[1]]);
await expect.soft(scopeTreeCheckboxes).toHaveCount(1);
expect.soft(await scopeTreeCheckboxes.first().locator('../..').textContent()).toBe(scopeSearchTwo);
});
});
}
);

View File

@ -0,0 +1,60 @@
import { Page } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
const USE_LIVE_DATA = Boolean(process.env.API_CONFIG_PATH);
const API_CONFIG_PATH = process.env.API_CONFIG_PATH ?? '../dashboards/cujs/config.json';
async function loadApiConfig() {
const configPath = path.resolve(__dirname, API_CONFIG_PATH);
if (configPath.endsWith('.json')) {
const configContent = await fs.promises.readFile(configPath, 'utf-8');
return JSON.parse(configContent);
} else {
const apiConfigModule = await import(configPath);
return apiConfigModule.default || apiConfigModule;
}
}
export async function prepareAPIMocks(page: Page) {
const apiConfig = await loadApiConfig();
if (USE_LIVE_DATA) {
return apiConfig;
}
const keys = Object.keys(apiConfig);
if (keys.includes('labels')) {
// mock the API call to get the labels
const labels = ['asserts_env', 'cluster', 'job'];
await page.route(apiConfig.labels, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
data: labels,
}),
});
});
}
if (keys.includes('values')) {
// mock the API call to get the values
const values = ['value1', 'value2', 'test1', 'test2'];
await page.route(apiConfig.values, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'success',
data: values,
}),
});
});
}
return apiConfig;
}

View File

@ -0,0 +1,4 @@
{
"labels": "**/resources/**/labels*",
"values": "**/resources/**/values*"
}

View File

@ -0,0 +1,169 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"mode": "markdown",
"content": "GroupByVar: $groupBy\n\nAdHocVar: $adHoc\n\nTimerange: ${__url_time_range}"
},
"pluginVersion": "8.4.0-pre",
"title": "Panel Title",
"type": "text"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 4,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": ["sum"],
"show": false
},
"showHeader": true
},
"pluginVersion": "12.2.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"queryType": "randomWalk",
"refId": "A"
}
],
"title": "Table panel",
"type": "table"
}
],
"schemaVersion": 41,
"tags": [],
"templating": {
"list": [
{
"baseFilters": [],
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"filters": [
{
"condition": "",
"key": "environment",
"keyLabel": "environment",
"matchAllFilter": false,
"operator": "=",
"origin": "dashboard",
"restorable": false,
"value": "prod",
"valueLabels": ["prod"],
"values": ["prod"]
},
{
"condition": "",
"key": "container",
"keyLabel": "container",
"operator": "=",
"value": "test",
"valueLabels": ["test"]
}
],
"name": "adHoc",
"type": "adhoc"
},
{
"current": {
"text": ["job"],
"value": ["job"]
},
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"defaultValue": {
"text": ["job"],
"value": ["job"]
},
"name": "groupBy",
"type": "groupby"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "CUJ Dashboard 1",
"uid": "cuj-dashboard-1",
"version": 3,
"weekStart": ""
}

View File

@ -0,0 +1,86 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"mode": "markdown",
"content": "GroupByVar: $groupBy\n\nAdHocVar: $adHoc\n\nTimerange: ${__url_time_range}"
},
"pluginVersion": "8.4.0-pre",
"title": "Panel Title",
"type": "text"
}
],
"schemaVersion": 41,
"tags": [],
"templating": {
"list": [
{
"baseFilters": [],
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"filters": [
{
"condition": "",
"key": "environment",
"keyLabel": "environment",
"operator": "=",
"value": "dev",
"valueLabels": ["dev"],
"values": ["dev"]
}
],
"name": "adHoc",
"type": "adhoc"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"name": "groupBy",
"type": "groupby"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "CUJ Dashboard 2",
"uid": "cuj-dashboard-2",
"version": 3,
"weekStart": ""
}

View File

@ -0,0 +1,76 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"mode": "markdown",
"content": "GroupByVar: $groupBy\n\nAdHocVar: $adHoc\n\nTimerange: ${__url_time_range}"
},
"pluginVersion": "8.4.0-pre",
"title": "Panel Title",
"type": "text"
}
],
"schemaVersion": 41,
"tags": [],
"templating": {
"list": [
{
"baseFilters": [],
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"filters": [],
"name": "adHoc",
"type": "adhoc"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "gdev-prometheus"
},
"name": "groupBy",
"type": "groupby"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "CUJ Dashboard 3",
"uid": "cuj-dashboard-3",
"version": 3,
"weekStart": ""
}

View File

@ -0,0 +1,295 @@
import { Page, Response } from '@playwright/test';
import { ScopeDashboardBindingSpec, ScopeDashboardBindingStatus } from '@grafana/data';
import { Resource } from '../../public/app/features/apiserver/types';
import { testScopes } from './scopes';
const USE_LIVE_DATA = Boolean(process.env.API_CALLS_CONFIG_PATH);
export type TestScope = {
name: string;
title: string;
children?: TestScope[];
filters?: Array<{ key: string; value: string; operator: string }>;
dashboardUid?: string;
dashboardTitle?: string;
disableMultiSelect?: boolean;
type?: string;
category?: string;
addLinks?: boolean;
};
type ScopeDashboardBinding = Resource<ScopeDashboardBindingSpec, ScopeDashboardBindingStatus, 'ScopeDashboardBinding'>;
export async function scopeNodeChildrenRequest(
page: Page,
scopes: TestScope[],
parentName?: string
): Promise<Response> {
await page.route(`**/apis/scope.grafana.app/v0alpha1/namespaces/*/find/scope_node_children*`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
apiVersion: 'scope.grafana.app/v0alpha1',
kind: 'FindScopeNodeChildrenResults',
metadata: {},
items: scopes.map((scope) => ({
kind: 'ScopeNode',
apiVersion: 'scope.grafana.app/v0alpha1',
metadata: {
name: `${scope.name}`,
namespace: 'default',
},
spec: {
title: scope.title,
description: scope.title,
disableMultiSelect: scope.disableMultiSelect ?? false,
nodeType: scope.children ? 'container' : 'leaf',
...(parentName && {
parentName,
}),
...((scope.addLinks || scope.children) && {
linkType: 'scope',
linkId: `scope-${scope.name}`,
}),
},
})),
}),
});
});
return page.waitForResponse((response) => response.url().includes(`/find/scope_node_children`));
}
export async function openScopesSelector(page: Page, scopes?: TestScope[]) {
const click = async () => await page.getByTestId('scopes-selector-input').click();
if (!scopes) {
await click();
return;
}
const responsePromise = scopeNodeChildrenRequest(page, scopes);
await click();
await responsePromise;
}
export async function expandScopesSelection(page: Page, parentScope: string, scopes?: TestScope[]) {
const click = async () => await page.getByTestId(`scopes-tree-${parentScope}-expand`).click();
if (!scopes) {
await click();
return;
}
const responsePromise = scopeNodeChildrenRequest(page, scopes, parentScope);
await click();
await responsePromise;
}
export async function scopeSelectRequest(page: Page, selectedScope: TestScope): Promise<Response> {
await page.route(
`**/apis/scope.grafana.app/v0alpha1/namespaces/*/scopes/scope-${selectedScope.name}`,
async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
kind: 'Scope',
apiVersion: 'scope.grafana.app/v0alpha1',
metadata: {
name: `scope-${selectedScope.name}`,
namespace: 'default',
},
spec: {
title: selectedScope.title,
description: '',
filters: selectedScope.filters,
category: selectedScope.category,
type: selectedScope.type,
},
}),
});
}
);
return page.waitForResponse((response) => response.url().includes(`/scopes/scope-${selectedScope.name}`));
}
export async function selectScope(page: Page, scopeName: string, selectedScope?: TestScope) {
const click = async () => {
const element = page.locator(
`[data-testid="scopes-tree-${scopeName}-checkbox"], [data-testid="scopes-tree-${scopeName}-radio"]`
);
await element.scrollIntoViewIfNeeded();
await element.click({ force: true });
};
if (!selectedScope) {
await click();
return;
}
const responsePromise = scopeSelectRequest(page, selectedScope);
await click();
await responsePromise;
}
export async function applyScopes(page: Page, scopes?: TestScope[]) {
const click = async () => {
await page.getByTestId('scopes-selector-apply').scrollIntoViewIfNeeded();
await page.getByTestId('scopes-selector-apply').click({ force: true });
};
if (!scopes) {
await click();
return;
}
const url: string =
'**/apis/scope.grafana.app/v0alpha1/namespaces/*/find/scope_dashboard_bindings?' +
scopes.map((scope) => `scope=scope-${scope.name}`).join('&');
const groups: string[] = ['Most relevant', 'Dashboards', 'Something else', ''];
await page.route(url, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
apiVersion: 'scope.grafana.app/v0alpha1',
items: scopes.flatMap((scope) => {
const bindings: ScopeDashboardBinding[] = [];
for (let i = 0; i < 10; i++) {
const selectedGroup = groups[Math.floor(Math.random() * groups.length)];
bindings.push({
kind: 'ScopeDashboardBinding',
apiVersion: 'scope.grafana.app/v0alpha1',
metadata: {
name: 'scope',
resourceVersion: '1',
creationTimestamp: 'stamp',
},
spec: {
dashboard: (scope.dashboardUid ?? 'edediimbjhdz4b') + '/' + Math.random().toString(),
scope: `scope-${scope.name}`,
},
status: {
dashboardTitle: (scope.dashboardTitle ?? 'A tall dashboard') + (selectedGroup[0] ?? 'U') + i,
...(selectedGroup !== '' && { groups: [selectedGroup] }),
},
});
}
// make sure there is always a binding with no group
bindings.push({
kind: 'ScopeDashboardBinding',
apiVersion: 'scope.grafana.app/v0alpha1',
metadata: {
name: 'scope',
resourceVersion: '1',
creationTimestamp: 'stamp',
},
spec: {
dashboard: (scope.dashboardUid ?? 'edediimbjhdz4b') + '/' + Math.random().toString(),
scope: `scope-${scope.name}`,
},
status: {
dashboardTitle: (scope.dashboardTitle ?? 'A tall dashboard') + 'U123',
},
});
return bindings;
}),
}),
});
});
const responsePromise = page.waitForResponse((response) => response.url().includes(`/find/scope_dashboard_bindings`));
const scopeRequestPromises: Array<Promise<Response>> = [];
for (const scope of scopes) {
scopeRequestPromises.push(scopeSelectRequest(page, scope));
}
await click();
await responsePromise;
await Promise.all(scopeRequestPromises);
}
export async function searchScopes(page: Page, value: string, resultScopes: TestScope[]) {
const click = async () => await page.getByTestId('scopes-tree-search').fill(value);
if (!resultScopes) {
await click();
return;
}
const responsePromise = scopeNodeChildrenRequest(page, resultScopes);
await click();
await responsePromise;
}
export async function getScopeTreeName(page: Page, nth: number): Promise<string> {
const locator = page.getByTestId(/^scopes-tree-.*-expand/).nth(nth);
const fullTestId = await locator.getAttribute('data-testid');
const scopeName = fullTestId?.replace(/^scopes-tree-/, '').replace(/-expand$/, '');
if (!scopeName) {
throw new Error('There are no scopes in the selector');
}
return scopeName;
}
export async function getScopeLeafName(page: Page, nth: number): Promise<string> {
const locator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio)/).nth(nth);
const fullTestId = await locator.getAttribute('data-testid');
const scopeName = fullTestId?.replace(/^scopes-tree-/, '').replace(/-(checkbox|radio)/, '');
if (!scopeName) {
throw new Error('There are no scopes in the selector');
}
return scopeName;
}
export async function getScopeLeafTitle(page: Page, nth: number): Promise<string> {
const locator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio)/).nth(nth);
const scopeTitle = await locator.locator('../..').textContent();
if (!scopeTitle) {
throw new Error('There are no scopes in the selector');
}
return scopeTitle;
}
export async function setScopes(page: Page, scopeBindingSetting?: { uid: string; title: string }) {
const scopes = testScopes(scopeBindingSetting);
await openScopesSelector(page, USE_LIVE_DATA ? undefined : scopes); //used only in mocked scopes version
let scopeName = await getScopeTreeName(page, 0);
const firstLevelScopes = scopes[0].children!; //used only in mocked scopes version
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : firstLevelScopes);
scopeName = await getScopeTreeName(page, 1);
const secondLevelScopes = firstLevelScopes[0].children!; //used only in mocked scopes version
await expandScopesSelection(page, scopeName, USE_LIVE_DATA ? undefined : secondLevelScopes);
const selectedScopes = [secondLevelScopes[0]]; //used only in mocked scopes version
scopeName = await getScopeLeafName(page, 0);
await selectScope(page, scopeName, USE_LIVE_DATA ? undefined : selectedScopes[0]);
await applyScopes(page, USE_LIVE_DATA ? undefined : selectedScopes); //used only in mocked scopes version
}

View File

@ -0,0 +1,107 @@
import { TestScope } from './scope-helpers';
export const testScopes = (scopeBindingSetting?: { uid: string; title: string }): TestScope[] => {
return [
{
name: 'sn-databases',
title: 'Databases',
children: [
{
name: 'sn-databases-m',
title: 'Mimir',
children: [
{
name: 'sn-databases-m-mimir-dev-10',
title: 'mimir-dev-10',
filters: [{ key: 'namespace', operator: 'equals', value: 'mimir-dev-10' }],
dashboardTitle: scopeBindingSetting?.title ?? 'CUJ Dashboard 2',
dashboardUid: scopeBindingSetting?.uid ?? 'cuj-dashboard-2',
type: 'type',
category: 'category',
addLinks: true,
},
{
name: 'sn-databases-m-mimir-dev-11',
title: 'mimir-dev-11',
filters: [{ key: 'namespace', operator: 'equals', value: 'mimir-dev-11' }],
category: 'category',
type: 'type',
addLinks: true,
},
],
},
{
name: 'sn-databases-l',
title: 'Loki',
children: [
{
name: 'sn-databases-l-loki-dev-010',
title: 'loki-dev-010',
filters: [{ key: 'namespace', operator: 'equals', value: 'loki-dev-010' }],
dashboardTitle: scopeBindingSetting?.title ?? 'CUJ Dashboard 2',
dashboardUid: scopeBindingSetting?.uid ?? 'cuj-dashboard-2',
addLinks: true,
},
{
name: 'sn-databases-l-loki-dev-009',
title: 'loki-dev-009',
filters: [{ key: 'namespace', operator: 'equals', value: 'loki-dev-009' }],
addLinks: true,
},
],
},
],
},
{
name: 'sn-hg',
title: 'Hosted Grafana',
children: [
{
name: 'sn-hg-c',
title: 'Cluster',
children: [
{
name: 'sn-hg-c-dev-eu-west-2-hosted-grafana',
title: 'dev-eu-west-2',
filters: [{ key: 'cluster', operator: 'equals', value: 'dev-eu-west-2' }],
dashboardTitle: scopeBindingSetting?.title ?? 'CUJ Dashboard 2',
dashboardUid: scopeBindingSetting?.uid ?? 'cuj-dashboard-2',
addLinks: true,
},
{
name: 'sn-hg-c-dev-us-central-0-hosted-grafana',
title: 'dev-us-central-0',
filters: [{ key: 'cluster', operator: 'equals', value: 'dev-us-central-0' }],
addLinks: true,
},
],
},
],
},
{
name: 'sn-other-teams',
title: 'Other teams',
children: [
{
name: 'sn-other-teams-t',
title: 'Test',
disableMultiSelect: true,
children: [
{
name: 'sn-other-teams-t-multi',
title: 'Multi group',
children: [],
addLinks: true,
},
{
name: 'sn-other-teams-t-another',
title: 'Another group',
addLinks: true,
filters: [],
},
],
},
],
},
];
};

View File

@ -593,6 +593,7 @@ export {
} from './types/pluginExtensions'; } from './types/pluginExtensions';
export { export {
type ScopeDashboardBindingSpec, type ScopeDashboardBindingSpec,
type ScopeDashboardBindingStatus,
type ScopeDashboardBinding, type ScopeDashboardBinding,
type ScopeFilterOperator, type ScopeFilterOperator,
type ScopeSpecFilter, type ScopeSpecFilter,

View File

@ -183,5 +183,25 @@ export default defineConfig<PluginOptions>({
name: 'dashboard-new-layouts', name: 'dashboard-new-layouts',
testDir: path.join(testDirRoot, '/dashboard-new-layouts'), testDir: path.join(testDirRoot, '/dashboard-new-layouts'),
}), }),
// Setup project for dashboard CUJS tests
withAuth({
name: 'dashboard-cujs-setup',
testDir: path.join(testDirRoot, '/dashboard-cujs'),
testMatch: ['global-setup.spec.ts'],
}),
// Main dashboard CUJS tests
withAuth({
name: 'dashboard-cujs',
testDir: path.join(testDirRoot, '/dashboard-cujs'),
testIgnore: ['global-setup.spec.ts', 'global-teardown.spec.ts'],
dependencies: ['dashboard-cujs-setup'],
}),
// Teardown project for dashboard CUJS tests
withAuth({
name: 'dashboard-cujs-teardown',
testDir: path.join(testDirRoot, '/dashboard-cujs'),
testMatch: ['global-teardown.spec.ts'],
dependencies: ['dashboard-cujs'],
}),
], ],
}); });