mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 10:24:54 -06:00
E2E: Add support for data-test-id over aria labels and add importDashboard flow (#36483)
* E2E: Add support for data-testids and not just aria-labels.
This commit is contained in:
parent
ef689d0c24
commit
f32d200fc0
@ -160,3 +160,41 @@ describe('List test', () => {
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Aria-Labels vs data-testid
|
||||
Our selectors are set up to work with both aria-labels and data-testid attributes. Aria-labels help assistive technologies such as screenreaders identify interactive elements of a page for our users.
|
||||
|
||||
A good example of a time to use an aria-label might be if you have a button with an X to close:
|
||||
```
|
||||
<button aria-label="close">X<button>
|
||||
```
|
||||
It might be clear visually that the X closes the modal, but audibly it would not be clear for example.
|
||||
```
|
||||
<button aria-label="close">Close<button>
|
||||
```
|
||||
The above example for example might read aloud to a user "Close, Close" or something similar.
|
||||
|
||||
However adding aria-labels to elements that are already clearly labeled or not interactive can be confusing and redundant for users with assistive technologies.
|
||||
|
||||
In such cases rather than adding unnecessary aria-labels to components so as to make them selectable for testing, it is preferable to use a data attribute that would not be read aloud with an assistive technology for example:
|
||||
|
||||
```
|
||||
<button data-testid="modal-close-button">Close<button>
|
||||
```
|
||||
|
||||
We have added support for this in our selectors, to use:
|
||||
|
||||
Prefix your selector string with "data-testid":
|
||||
```typescript
|
||||
export const Components = {
|
||||
Login: {
|
||||
openButton: "data-testid-open", // this would look for a data-testid
|
||||
closeButton: "close-button" // this would look for an aria-label
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
and in your component, import the selectors and add the data test id:
|
||||
```
|
||||
<button data-testid={Selectors.Components.Login.openButton}>
|
||||
```
|
122
e2e/suite1/specs/import-dashboard.spec.ts
Normal file
122
e2e/suite1/specs/import-dashboard.spec.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Import Dashboard Test',
|
||||
itName: 'Ensure you can import a dashboard',
|
||||
addScenarioDataSource: false,
|
||||
addScenarioDashBoard: false,
|
||||
skipScenario: false,
|
||||
scenario: () => {
|
||||
e2e.flows.importDashboard(TEST_DASHBOARD);
|
||||
},
|
||||
});
|
||||
|
||||
const TEST_DASHBOARD = {
|
||||
annotations: {
|
||||
list: [
|
||||
{
|
||||
builtIn: 1,
|
||||
datasource: '-- Grafana --',
|
||||
enable: true,
|
||||
hide: true,
|
||||
iconColor: 'rgba(0, 211, 255, 1)',
|
||||
name: 'Annotations & Alerts',
|
||||
type: 'dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
editable: true,
|
||||
gnetId: null,
|
||||
graphTooltip: 0,
|
||||
id: 74,
|
||||
links: [],
|
||||
panels: [
|
||||
{
|
||||
datasource: null,
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
color: {
|
||||
mode: 'palette-classic',
|
||||
},
|
||||
custom: {
|
||||
axisLabel: '',
|
||||
axisPlacement: 'auto',
|
||||
barAlignment: 0,
|
||||
drawStyle: 'line',
|
||||
fillOpacity: 0,
|
||||
gradientMode: 'none',
|
||||
hideFrom: {
|
||||
legend: false,
|
||||
tooltip: false,
|
||||
viz: false,
|
||||
},
|
||||
lineInterpolation: 'linear',
|
||||
lineWidth: 1,
|
||||
pointSize: 5,
|
||||
scaleDistribution: {
|
||||
type: 'linear',
|
||||
},
|
||||
showPoints: 'auto',
|
||||
spanNulls: false,
|
||||
stacking: {
|
||||
group: 'A',
|
||||
mode: 'none',
|
||||
},
|
||||
thresholdsStyle: {
|
||||
mode: 'off',
|
||||
},
|
||||
},
|
||||
mappings: [],
|
||||
thresholds: {
|
||||
mode: 'absolute',
|
||||
steps: [
|
||||
{
|
||||
color: 'green',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
value: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
gridPos: {
|
||||
h: 9,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
id: 2,
|
||||
options: {
|
||||
legend: {
|
||||
calcs: [],
|
||||
displayMode: 'list',
|
||||
placement: 'bottom',
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'single',
|
||||
},
|
||||
},
|
||||
title: 'Panel Title',
|
||||
type: 'timeseries',
|
||||
},
|
||||
],
|
||||
schemaVersion: 30,
|
||||
style: 'dark',
|
||||
tags: [],
|
||||
templating: {
|
||||
list: [],
|
||||
},
|
||||
time: {
|
||||
from: '2021-06-30T04:00:00.000Z',
|
||||
to: '2021-07-02T03:59:59.000Z',
|
||||
},
|
||||
timepicker: {},
|
||||
timezone: '',
|
||||
title: 'An imported dashboard for e2e tests',
|
||||
uid: '6V0Nzyz7k',
|
||||
version: 1,
|
||||
};
|
@ -1,3 +1,9 @@
|
||||
// NOTE: by default Component string selectors are set up to be aria-labels,
|
||||
// however there are many cases where your component may not need an aria-label
|
||||
// (a <button> with clear text, for example, does not need an aria-label as it's already labeled)
|
||||
// but you still might need to select it for testing,
|
||||
// in that case please add the attribute data-test-id={selector} in the component and
|
||||
// prefix your selector string with 'data-test-id' so that when create the selectors we know to search for it on the right attribute
|
||||
export const Components = {
|
||||
TimePicker: {
|
||||
openButton: 'TimePicker Open Button',
|
||||
@ -222,4 +228,12 @@ export const Components = {
|
||||
CodeEditor: {
|
||||
container: 'Code editor container',
|
||||
},
|
||||
DashboardImportPage: {
|
||||
textarea: 'data-testid-import-dashboard-textarea',
|
||||
submit: 'data-testid-load-dashboard',
|
||||
},
|
||||
ImportDashboardForm: {
|
||||
name: 'data-testid-import-dashboard-title',
|
||||
submit: 'data-testid-import-dashboard-submit',
|
||||
},
|
||||
};
|
||||
|
55
packages/grafana-e2e/src/flows/importDashboard.ts
Normal file
55
packages/grafana-e2e/src/flows/importDashboard.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { DeleteDashboardConfig } from '.';
|
||||
import { e2e } from '../index';
|
||||
import { fromBaseUrl, getDashboardUid } from '../support/url';
|
||||
|
||||
type Panel = {
|
||||
title: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type Dashboard = { title: string; panels: Panel[]; [key: string]: unknown };
|
||||
|
||||
/**
|
||||
* Smoke test a datasource by quickly importing a test dashboard for it
|
||||
* @param dashboardToImport a sample dashboard
|
||||
*/
|
||||
export const importDashboard = (dashboardToImport: Dashboard) => {
|
||||
e2e().visit(fromBaseUrl('/dashboard/import'));
|
||||
|
||||
// Note: normally we'd use 'click' and then 'type' here, but the json object is so big that using 'val' is much faster
|
||||
e2e.components.DashboardImportPage.textarea().click({ force: true }).invoke('val', JSON.stringify(dashboardToImport));
|
||||
e2e.components.DashboardImportPage.submit().click({ force: true });
|
||||
e2e.components.ImportDashboardForm.name().click({ force: true }).clear().type(dashboardToImport.title);
|
||||
e2e.components.ImportDashboardForm.submit().click({ force: true });
|
||||
e2e().wait(3000);
|
||||
|
||||
// save the newly imported dashboard to context so it'll get properly deleted later
|
||||
e2e()
|
||||
.url()
|
||||
.should('contain', '/d/')
|
||||
.then((url: string) => {
|
||||
const uid = getDashboardUid(url);
|
||||
|
||||
e2e.getScenarioContext().then(({ addedDashboards }: { addedDashboards: DeleteDashboardConfig[] }) => {
|
||||
e2e.setScenarioContext({
|
||||
addedDashboards: [...addedDashboards, { title: dashboardToImport.title, uid }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// inspect first panel and verify data has been processed for it
|
||||
e2e.components.Panels.Panel.title(dashboardToImport.panels[0].title).click({ force: true });
|
||||
e2e.components.Panels.Panel.headerItems('Inspect').click({ force: true });
|
||||
e2e.components.Tab.title('JSON').click({ force: true });
|
||||
e2e().wait(3000);
|
||||
e2e.components.PanelInspector.Json.content().contains('Panel JSON').click({ force: true });
|
||||
e2e().wait(3000);
|
||||
e2e.components.Select.option().contains('Data').click({ force: true });
|
||||
e2e().wait(3000);
|
||||
|
||||
// ensures that panel has loaded without knowingly hitting an error
|
||||
// note: this does not prove that data came back as we expected it,
|
||||
// it could get `state: Done` for no data for example
|
||||
// but it ensures we didn't hit a 401 or 500 or something like that
|
||||
e2e.components.CodeEditor.container().contains('"state": "Done"');
|
||||
};
|
@ -12,6 +12,7 @@ export * from './openPanelMenuItem';
|
||||
export * from './revertAllChanges';
|
||||
export * from './saveDashboard';
|
||||
export * from './selectOption';
|
||||
export * from './importDashboard';
|
||||
|
||||
export {
|
||||
VISUALIZATION_ALERT_LIST,
|
||||
|
@ -1,9 +1,11 @@
|
||||
export interface SelectorApi {
|
||||
fromAriaLabel: (selector: string) => string;
|
||||
fromDataTestId: (selector: string) => string;
|
||||
fromSelector: (selector: string) => string;
|
||||
}
|
||||
|
||||
export const Selector: SelectorApi = {
|
||||
fromAriaLabel: (selector: string) => `[aria-label="${selector}"]`,
|
||||
fromDataTestId: (selector: string) => `[data-testid="${selector}"]`,
|
||||
fromSelector: (selector: string) => selector,
|
||||
};
|
||||
|
@ -61,7 +61,11 @@ const processSelectors = <S extends Selectors>(e2eObjects: E2EFunctions<S>, sele
|
||||
// @ts-ignore
|
||||
e2eObjects[key] = (options?: CypressOptions) => {
|
||||
logOutput(value);
|
||||
return e2e().get(Selector.fromAriaLabel(value), options);
|
||||
const selector = value.startsWith('data-testid')
|
||||
? Selector.fromDataTestId(value)
|
||||
: Selector.fromAriaLabel(value);
|
||||
|
||||
return e2e().get(selector, options);
|
||||
};
|
||||
|
||||
continue;
|
||||
@ -81,8 +85,10 @@ const processSelectors = <S extends Selectors>(e2eObjects: E2EFunctions<S>, sele
|
||||
// the input can be (text) or (options)
|
||||
if (arguments.length === 1) {
|
||||
if (typeof textOrOptions === 'string') {
|
||||
const ariaText = value(textOrOptions);
|
||||
const selector = Selector.fromAriaLabel(ariaText);
|
||||
const selectorText = value(textOrOptions);
|
||||
const selector = selectorText.startsWith('data-testid')
|
||||
? Selector.fromDataTestId(selectorText)
|
||||
: Selector.fromAriaLabel(selectorText);
|
||||
|
||||
logOutput(selector);
|
||||
return e2e().get(selector);
|
||||
@ -94,9 +100,12 @@ const processSelectors = <S extends Selectors>(e2eObjects: E2EFunctions<S>, sele
|
||||
}
|
||||
|
||||
// the input can only be (text, options)
|
||||
if (arguments.length === 2) {
|
||||
const ariaText = value(textOrOptions as string);
|
||||
const selector = Selector.fromAriaLabel(ariaText);
|
||||
if (arguments.length === 2 && typeof textOrOptions === 'string') {
|
||||
const text = textOrOptions;
|
||||
const selectorText = value(text);
|
||||
const selector = text.startsWith('data-testid')
|
||||
? Selector.fromDataTestId(selectorText)
|
||||
: Selector.fromAriaLabel(selectorText);
|
||||
|
||||
logOutput(selector);
|
||||
return e2e().get(selector, options);
|
||||
|
@ -2,6 +2,7 @@ import React, { FormEvent, PureComponent } from 'react';
|
||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { css } from '@emotion/css';
|
||||
import { AppEvents, GrafanaTheme2, NavModel } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import {
|
||||
Button,
|
||||
stylesFactory,
|
||||
@ -121,10 +122,13 @@ class UnthemedDashboardImport extends PureComponent<Props> {
|
||||
required: 'Need a dashboard JSON model',
|
||||
validate: validateDashboardJson,
|
||||
})}
|
||||
data-testid={selectors.components.DashboardImportPage.textarea}
|
||||
rows={10}
|
||||
/>
|
||||
</Field>
|
||||
<Button type="submit">Load</Button>
|
||||
<Button type="submit" data-testid={selectors.components.DashboardImportPage.submit}>
|
||||
Load
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
@ -14,6 +14,7 @@ import { DataSourcePicker } from '@grafana/runtime';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers';
|
||||
import { validateTitle, validateUid } from '../utils/validation';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
interface Props extends Pick<FormAPI<ImportDashboardDTO>, 'register' | 'errors' | 'control' | 'getValues' | 'watch'> {
|
||||
uidReset: boolean;
|
||||
@ -61,6 +62,7 @@ export const ImportDashboardForm: FC<Props> = ({
|
||||
validate: async (v: string) => await validateTitle(v, getValues().folder.id),
|
||||
})}
|
||||
type="text"
|
||||
data-testid={selectors.components.ImportDashboardForm.name}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Folder">
|
||||
@ -137,6 +139,7 @@ export const ImportDashboardForm: FC<Props> = ({
|
||||
<HorizontalGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid={selectors.components.ImportDashboardForm.submit}
|
||||
variant={getButtonVariant(errors)}
|
||||
onClick={() => {
|
||||
setSubmitted(true);
|
||||
|
Loading…
Reference in New Issue
Block a user