mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
E2E Selectors: Add minimum Grafana version to selectors (#93468)
* make selectors versioned * update readme
This commit is contained in:
parent
5a9de531d2
commit
58f180d58b
@ -422,6 +422,9 @@ exports[`better eslint`] = {
|
|||||||
"packages/grafana-data/test/__mocks__/pluginMocks.ts:5381": [
|
"packages/grafana-data/test/__mocks__/pluginMocks.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[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": [
|
"packages/grafana-o11y-ds-frontend/src/utils.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "15.3.0",
|
"@rollup/plugin-node-resolve": "15.3.0",
|
||||||
"@types/node": "20.16.15",
|
"@types/node": "20.16.15",
|
||||||
|
"@types/semver": "7.5.8",
|
||||||
"esbuild": "0.24.0",
|
"esbuild": "0.24.0",
|
||||||
"rimraf": "6.0.1",
|
"rimraf": "6.0.1",
|
||||||
"rollup": "^4.22.4",
|
"rollup": "^4.22.4",
|
||||||
@ -50,6 +51,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grafana/tsconfig": "^2.0.0",
|
"@grafana/tsconfig": "^2.0.0",
|
||||||
|
"semver": "7.6.3",
|
||||||
"tslib": "2.7.0",
|
"tslib": "2.7.0",
|
||||||
"typescript": "5.5.4"
|
"typescript": "5.5.4"
|
||||||
}
|
}
|
||||||
|
@ -5,3 +5,4 @@
|
|||||||
*/
|
*/
|
||||||
export * from './selectors';
|
export * from './selectors';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
export { resolveSelectors } from './resolver';
|
||||||
|
68
packages/grafana-e2e-selectors/src/resolver.test.ts
Normal file
68
packages/grafana-e2e-selectors/src/resolver.test.ts
Normal 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'"));
|
||||||
|
});
|
||||||
|
});
|
81
packages/grafana-e2e-selectors/src/resolver.ts
Normal file
81
packages/grafana-e2e-selectors/src/resolver.ts
Normal 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}'`);
|
||||||
|
}
|
||||||
|
}
|
38
packages/grafana-e2e-selectors/src/selectors/README.md
Normal file
38
packages/grafana-e2e-selectors/src/selectors/README.md
Normal 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
@ -0,0 +1 @@
|
|||||||
|
export const MIN_GRAFANA_VERSION = '8.5.0';
|
@ -1,21 +1,24 @@
|
|||||||
|
import { resolveSelectors } from '../resolver';
|
||||||
import { E2ESelectors } from '../types';
|
import { E2ESelectors } from '../types';
|
||||||
|
|
||||||
import { Components } from './components';
|
import { versionedComponents, VersionedComponents } from './components';
|
||||||
import { Pages } from './pages';
|
import { versionedPages, VersionedPages } from './pages';
|
||||||
|
|
||||||
/**
|
const Pages = resolveSelectors(versionedPages);
|
||||||
* Exposes selectors in package for easy use in e2e tests and in production code
|
const Components = resolveSelectors(versionedComponents);
|
||||||
*
|
const selectors = { pages: Pages, components: Components };
|
||||||
* @alpha
|
|
||||||
*/
|
|
||||||
export const selectors: { pages: E2ESelectors<typeof Pages>; components: E2ESelectors<typeof Components> } = {
|
|
||||||
pages: Pages,
|
|
||||||
components: Components,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exposes Pages, Component selectors and E2ESelectors type in package for easy use in e2e tests and in production code
|
* 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
@ -1,42 +1,67 @@
|
|||||||
/**
|
/**
|
||||||
* A string selector
|
* A string selector
|
||||||
*
|
|
||||||
* @alpha
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type StringSelector = string;
|
export type StringSelector = string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function selector with an argument
|
* A function selector with one argument
|
||||||
*
|
|
||||||
* @alpha
|
|
||||||
*/
|
*/
|
||||||
export type FunctionSelector = (id: string) => string;
|
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
|
* A function selector without argument
|
||||||
*
|
|
||||||
* @alpha
|
|
||||||
*/
|
*/
|
||||||
export type CssSelector = () => string;
|
export type CssSelector = () => string;
|
||||||
|
|
||||||
/**
|
|
||||||
* @alpha
|
|
||||||
*/
|
|
||||||
export interface Selectors {
|
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> = {
|
export type E2ESelectors<S extends Selectors> = {
|
||||||
[P in keyof S]: S[P];
|
[P in keyof S]: S[P];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @alpha
|
|
||||||
*/
|
|
||||||
export interface UrlSelector extends Selectors {
|
export interface UrlSelector extends Selectors {
|
||||||
url: string | FunctionSelector;
|
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]>;
|
||||||
|
};
|
||||||
|
@ -3609,12 +3609,14 @@ __metadata:
|
|||||||
"@grafana/tsconfig": "npm:^2.0.0"
|
"@grafana/tsconfig": "npm:^2.0.0"
|
||||||
"@rollup/plugin-node-resolve": "npm:15.3.0"
|
"@rollup/plugin-node-resolve": "npm:15.3.0"
|
||||||
"@types/node": "npm:20.16.15"
|
"@types/node": "npm:20.16.15"
|
||||||
|
"@types/semver": "npm:7.5.8"
|
||||||
esbuild: "npm:0.24.0"
|
esbuild: "npm:0.24.0"
|
||||||
rimraf: "npm:6.0.1"
|
rimraf: "npm:6.0.1"
|
||||||
rollup: "npm:^4.22.4"
|
rollup: "npm:^4.22.4"
|
||||||
rollup-plugin-dts: "npm:^6.1.1"
|
rollup-plugin-dts: "npm:^6.1.1"
|
||||||
rollup-plugin-esbuild: "npm:6.1.1"
|
rollup-plugin-esbuild: "npm:6.1.1"
|
||||||
rollup-plugin-node-externals: "npm:^7.1.3"
|
rollup-plugin-node-externals: "npm:^7.1.3"
|
||||||
|
semver: "npm:7.6.3"
|
||||||
tslib: "npm:2.7.0"
|
tslib: "npm:2.7.0"
|
||||||
typescript: "npm:5.5.4"
|
typescript: "npm:5.5.4"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
|
Loading…
Reference in New Issue
Block a user