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
20 changed files with 1884 additions and 0 deletions
+295
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
}
+107
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: [],
},
],
},
],
},
];
};