E2E Selectors: Add minimum Grafana version to selectors (#93468)

* make selectors versioned

* update readme
This commit is contained in:
Erik Sundell 2024-10-24 10:28:21 +02:00 committed by GitHub
parent 5a9de531d2
commit 58f180d58b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 2084 additions and 686 deletions

View File

@ -422,6 +422,9 @@ exports[`better eslint`] = {
"packages/grafana-data/test/__mocks__/pluginMocks.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-e2e-selectors/src/resolver.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-o11y-ds-frontend/src/utils.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],

View File

@ -41,6 +41,7 @@
"devDependencies": {
"@rollup/plugin-node-resolve": "15.3.0",
"@types/node": "20.16.15",
"@types/semver": "7.5.8",
"esbuild": "0.24.0",
"rimraf": "6.0.1",
"rollup": "^4.22.4",
@ -50,6 +51,7 @@
},
"dependencies": {
"@grafana/tsconfig": "^2.0.0",
"semver": "7.6.3",
"tslib": "2.7.0",
"typescript": "5.5.4"
}

View File

@ -5,3 +5,4 @@
*/
export * from './selectors';
export * from './types';
export { resolveSelectors } from './resolver';

View File

@ -0,0 +1,68 @@
import { resolveSelectors } from './resolver';
import { versionedComponents } from './selectors/components';
import { versionedPages } from './selectors/pages';
describe('Resolver', () => {
it('should resolve latest when no version is provided', () => {
const pages = resolveSelectors(versionedPages);
const components = resolveSelectors(versionedComponents);
expect(components.CodeEditor.container).toBe('data-testid Code editor container');
expect(components.TimePicker.calendar.closeButton).toBe('data-testid Close time range Calendar');
expect(components.Panels.Panel.status('Test')).toBe('data-testid Panel status Test');
expect(components.PanelEditor.OptionsPane.fieldInput('Test')).toBe(
'data-testid Panel editor option pane field input Test'
);
expect(pages.Alerting.AddAlertRule.url).toBe('/alerting/new/alerting');
expect(pages.AddDashboard.Settings.Variables.Edit.url('test')).toBe(
'/dashboard/new?orgId=1&editview=templating&editIndex=test'
);
});
it('should resolve right selector versions when an older grafana version is provided', () => {
const pages = resolveSelectors(versionedPages, '10.0.14');
const components = resolveSelectors(versionedComponents, '10.0.14');
expect(components.CodeEditor.container).toBe('Code editor container');
expect(components.TimePicker.calendar.closeButton).toBe('Close time range Calendar');
expect(components.Panels.Panel.status('')).toBe('Panel status');
expect(components.PanelEditor.OptionsPane.fieldInput('Test')).toBe(
'data-testid Panel editor option pane field input Test'
);
expect(pages.Alerting.AddAlertRule.url).toBe('/alerting/new');
expect(pages.AddDashboard.Settings.Variables.Edit.url('test')).toBe(
'/dashboard/new?orgId=1&editview=templating&editIndex=test'
);
});
it('should resolve the most recent selector version when a newer grafana version is provided', () => {
const pages = resolveSelectors(
{
Alerting: {
AddAlertRule: {
url: {
'11.4.0': '/alerting/new',
'11.1.0': '/alerting/old',
'9.0.15': '/alerting/new/alerting',
},
},
},
},
'25.1.0'
);
expect(pages.Alerting.AddAlertRule.url).toBe('/alerting/new');
});
it('should throw error if an invalid semver range is used in versioned selector', () => {
expect(() =>
resolveSelectors({
Alerting: {
AddAlertRule: {
url: {
abc: '/alerting/new',
'9.0.15': '/alerting/new/alerting',
},
},
},
})
).toThrow(new Error("Invalid semver version: 'abc'"));
});
});

View File

@ -0,0 +1,81 @@
import { gte, compare, valid } from 'semver';
import {
FunctionSelector,
Selectors,
SelectorsOf,
StringSelector,
VersionedSelectorGroup,
VersionedSelectors,
CssSelector,
UrlSelector,
FunctionSelectorTwoArgs,
} from './types';
/**
* Resolves selectors based on the Grafana version
*/
export function resolveSelectors<T extends VersionedSelectorGroup>(
versionedSelectors: T,
grafanaVersion = 'latest'
): SelectorsOf<T> {
const version = grafanaVersion.replace(/\-.*/, '');
return resolveSelectorGroup(versionedSelectors, version);
}
function resolveSelectorGroup<T extends VersionedSelectorGroup>(group: T, grafanaVersion: string): SelectorsOf<T> {
const result: Selectors = {};
for (const [key, value] of Object.entries(group)) {
if (isVersionedSelectorGroup(value)) {
result[key] = resolveSelectorGroup(value, grafanaVersion);
} else {
assertIsSemverValid(value, key);
result[key] = resolveSelector(value, grafanaVersion);
}
}
return result as SelectorsOf<T>;
}
function isVersionedSelectorGroup(
target: VersionedSelectors | VersionedSelectorGroup
): target is VersionedSelectorGroup {
if (typeof target === 'object') {
const [first] = Object.keys(target);
return !valid(first);
}
return false;
}
function resolveSelector(
versionedSelector: VersionedSelectors,
grafanaVersion: string
): StringSelector | FunctionSelector | FunctionSelectorTwoArgs | CssSelector | UrlSelector {
let versionToUse;
let versions = Object.keys(versionedSelector).sort(compare);
if (grafanaVersion === 'latest') {
return versionedSelector[versions[versions.length - 1]];
}
for (const version of versions) {
if (gte(grafanaVersion, version)) {
versionToUse = version;
}
}
if (!versionToUse) {
versionToUse = versions[versions.length - 1];
}
return versionedSelector[versionToUse];
}
function assertIsSemverValid(versionedSelector: VersionedSelectors, selectorName: string) {
if (!Object.keys(versionedSelector).every((version) => valid(version))) {
throw new Error(`Invalid semver version: '${selectorName}'`);
}
}

View File

@ -0,0 +1,38 @@
# Versioned selectors
The selectors defined in [pages.ts](./pages.ts) and [components.ts](./components.ts) are versioned. A versioned selector consists of an object literal where value is the selector context and key is the minimum Grafana version for which the value is valid. The versioning is important in plugin end-to-end testing, as it allows them to resolve the right selector values for a given Grafana version.
```typescript
const components = {
PanelEditor: {
content: {
'11.1.0': 'data-testid Panel editor content', // resolved for Grafana >= 11.1.0
'9.5.0': 'Panel editor content', // resolved for Grafana >= 9.5.0 <11.1.0
},
}
...
}
```
A few things to keep in mind:
- Strive to use e2e selector for all components in grafana/ui.
- Only create new selector in case you're creating a new piece of UI. If you're changing an existing piece of UI that already has a selector defined, you need to keep using that selector. Otherwise you might break plugin end-to-end tests.
- Prefer using string selectors in favour of function selectors. The purpose of the selectors is to provide a canonical way to select elements.
`pages.Dashboard.url('ud73s9')` is fine.
`components.Panels.Panel.title('Panel header')` is bad.
## How to change the value of an existing selector
1. Find the versioned selector object in [pages.ts](./pages.ts) or [components.ts](./components.ts).
2. Add a new key representing the minimum Grafana version. The version you specify should correspond to the version of Grafana where your changes will be released. In most cases, you can check the version specified in package.json of the main branch (`git show main:package.json | awk -F'"' '/"version": ".+"/{ print $4; exit; }'`), but if you know in advance that your change will be backported you can specify the version of the release with the lowest version number. The version you specify should not include tags such as `-pre` or build number.
3. Add a value for the selector. Remember that the selector needs to be backwards compatible, so you cannot change its signature.
## How to add a new selector
> [!CAUTION]
> If you're changing a part of the UI that already has a selector defined, you should reuse the existing selector to avoid breaking end-to-end tests in plugins.
1. Add a new versioned selector object under an existing or new group in [pages.ts](./pages.ts) or [components.ts](./components.ts).
2. Add a new key representing the minimum Grafana version. The version you specify should correspond to the version of Grafana where this change will be released. You can check the version specified in package.json of the main branch (`git show main:package.json | awk -F'"' '/"version": ".+"/{ print $4; exit; }'`). The version you specify should not include tags such as `-pre` or build number.
3. Add a value for the selector. Prefer using string selectors as function selectors such as `selectors.components.Panels.Panel.title('Header')` require context awareness which makes them hard to use in end-to-end tests.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
export const MIN_GRAFANA_VERSION = '8.5.0';

View File

@ -1,21 +1,24 @@
import { resolveSelectors } from '../resolver';
import { E2ESelectors } from '../types';
import { Components } from './components';
import { Pages } from './pages';
import { versionedComponents, VersionedComponents } from './components';
import { versionedPages, VersionedPages } from './pages';
/**
* Exposes selectors in package for easy use in e2e tests and in production code
*
* @alpha
*/
export const selectors: { pages: E2ESelectors<typeof Pages>; components: E2ESelectors<typeof Components> } = {
pages: Pages,
components: Components,
};
const Pages = resolveSelectors(versionedPages);
const Components = resolveSelectors(versionedComponents);
const selectors = { pages: Pages, components: Components };
/**
* Exposes Pages, Component selectors and E2ESelectors type in package for easy use in e2e tests and in production code
*
* @alpha
*/
export { Pages, Components, type E2ESelectors };
export {
Pages,
Components,
selectors,
versionedComponents,
versionedPages,
resolveSelectors,
type VersionedPages,
type VersionedComponents,
type E2ESelectors,
};

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,67 @@
/**
* A string selector
*
* @alpha
*/
export type StringSelector = string;
/**
* A function selector with an argument
*
* @alpha
* A function selector with one argument
*/
export type FunctionSelector = (id: string) => string;
/**
* A function selector with two arguments
*/
export type FunctionSelectorTwoArgs = (arg1: string, arg2: string) => string;
/**
* A function selector without argument
*
* @alpha
*/
export type CssSelector = () => string;
/**
* @alpha
*/
export interface Selectors {
[key: string]: StringSelector | FunctionSelector | CssSelector | UrlSelector | Selectors;
[key: string]: StringSelector | FunctionSelector | FunctionSelectorTwoArgs | CssSelector | UrlSelector | Selectors;
}
/**
* @alpha
*/
export type E2ESelectors<S extends Selectors> = {
[P in keyof S]: S[P];
};
/**
* @alpha
*/
export interface UrlSelector extends Selectors {
url: string | FunctionSelector;
}
export type VersionedFunctionSelector1 = Record<string, FunctionSelector>;
export type VersionedFunctionSelector2 = Record<string, FunctionSelectorTwoArgs>;
export type VersionedStringSelector = Record<string, StringSelector>;
export type VersionedCssSelector = Record<string, CssSelector>;
export type VersionedUrlSelector = Record<string, UrlSelector>;
export type VersionedSelectors =
| VersionedFunctionSelector1
| VersionedFunctionSelector2
| VersionedStringSelector
| VersionedCssSelector
| VersionedUrlSelector;
export type VersionedSelectorGroup = {
[property: string]: VersionedSelectors | VersionedSelectorGroup;
};
export type SelectorsOf<T> = {
[Property in keyof T]: T[Property] extends VersionedFunctionSelector1
? FunctionSelector
: T[Property] extends VersionedFunctionSelector2
? FunctionSelectorTwoArgs
: T[Property] extends VersionedStringSelector
? StringSelector
: T[Property] extends VersionedCssSelector
? CssSelector
: T[Property] extends VersionedUrlSelector
? UrlSelector
: SelectorsOf<T[Property]>;
};

View File

@ -3609,12 +3609,14 @@ __metadata:
"@grafana/tsconfig": "npm:^2.0.0"
"@rollup/plugin-node-resolve": "npm:15.3.0"
"@types/node": "npm:20.16.15"
"@types/semver": "npm:7.5.8"
esbuild: "npm:0.24.0"
rimraf: "npm:6.0.1"
rollup: "npm:^4.22.4"
rollup-plugin-dts: "npm:^6.1.1"
rollup-plugin-esbuild: "npm:6.1.1"
rollup-plugin-node-externals: "npm:^7.1.3"
semver: "npm:7.6.3"
tslib: "npm:2.7.0"
typescript: "npm:5.5.4"
languageName: unknown