Files
grafana/e2e-playwright/utils/scope-helpers.ts
Victor Marin 27b3137baf 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
2025-09-04 15:17:54 +03:00

296 lines
9.4 KiB
TypeScript

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
}