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:
Sarah Zinger 2021-07-19 14:01:31 -04:00 committed by GitHub
parent ef689d0c24
commit f32d200fc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 255 additions and 7 deletions

View File

@ -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}>
```

View 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,
};

View File

@ -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',
},
};

View 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"');
};

View File

@ -12,6 +12,7 @@ export * from './openPanelMenuItem';
export * from './revertAllChanges';
export * from './saveDashboard';
export * from './selectOption';
export * from './importDashboard';
export {
VISUALIZATION_ALERT_LIST,

View File

@ -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,
};

View File

@ -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);

View File

@ -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>

View File

@ -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);