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:
@@ -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
|
||||
}
|
||||
@@ -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: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
Reference in New Issue
Block a user