Add a11y test coverage to Rule Creation Flow for Detections tab (#94377)

[Security Solution] Add a11y test coverage to Detections rule creation flow (#80060)
This commit is contained in:
Ece Özalp 2021-03-25 15:05:23 -04:00 committed by GitHub
parent c5e3e78de8
commit d70d02ee83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 330 additions and 12 deletions

View file

@ -463,6 +463,21 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
async getWelcomeText() {
return await testSubjects.getVisibleText('global-banner-item');
}
/**
* Clicks on an element, and validates that the desired effect has taken place
* by confirming the existence of a validator
*/
async clickAndValidate(
clickTarget: string,
validator: string,
isValidatorCssString: boolean = false,
topOffset?: number
) {
await testSubjects.click(clickTarget, undefined, topOffset);
const validate = isValidatorCssString ? find.byCssSelector : testSubjects.exists;
await validate(validator);
}
}
return new CommonPage();

View file

@ -79,11 +79,11 @@ export async function FindProvider({ getService }: FtrProviderContext) {
return wrap(await driver.switchTo().activeElement());
}
public async setValue(selector: string, text: string): Promise<void> {
public async setValue(selector: string, text: string, topOffset?: number): Promise<void> {
log.debug(`Find.setValue('${selector}', '${text}')`);
return await retry.try(async () => {
const element = await this.byCssSelector(selector);
await element.click();
await element.click(topOffset);
// in case the input element is actually a child of the testSubject, we
// call clearValue() and type() on the element that is focused after
@ -413,14 +413,15 @@ export async function FindProvider({ getService }: FtrProviderContext) {
public async clickByCssSelector(
selector: string,
timeout: number = defaultFindTimeout
timeout: number = defaultFindTimeout,
topOffset?: number
): Promise<void> {
log.debug(`Find.clickByCssSelector('${selector}') with timeout=${timeout}`);
await retry.try(async () => {
const element = await this.byCssSelector(selector, timeout);
if (element) {
// await element.moveMouseTo();
await element.click();
await element.click(topOffset);
} else {
throw new Error(`Element with css='${selector}' is not found`);
}

View file

@ -100,9 +100,13 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) {
await find.clickByCssSelectorWhenNotDisabled(testSubjSelector(selector), { timeout });
}
public async click(selector: string, timeout: number = FIND_TIME): Promise<void> {
public async click(
selector: string,
timeout: number = FIND_TIME,
topOffset?: number
): Promise<void> {
log.debug(`TestSubjects.click(${selector})`);
await find.clickByCssSelector(testSubjSelector(selector), timeout);
await find.clickByCssSelector(testSubjSelector(selector), timeout, topOffset);
}
public async doubleClick(selector: string, timeout: number = FIND_TIME): Promise<void> {
@ -187,12 +191,13 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) {
public async setValue(
selector: string,
text: string,
options: SetValueOptions = {}
options: SetValueOptions = {},
topOffset?: number
): Promise<void> {
return await retry.try(async () => {
const { clearWithKeyboard = false, typeCharByChar = false } = options;
log.debug(`TestSubjects.setValue(${selector}, ${text})`);
await this.click(selector);
await this.click(selector, undefined, topOffset);
// in case the input element is actually a child of the testSubject, we
// call clearValue() and type() on the element that is focused after
// clicking on the testSubject

View file

@ -182,9 +182,9 @@ export class WebElementWrapper {
*
* @return {Promise<void>}
*/
public async click() {
public async click(topOffset?: number) {
await this.retryCall(async function click(wrapper) {
await wrapper.scrollIntoViewIfNecessary();
await wrapper.scrollIntoViewIfNecessary(topOffset);
await wrapper._webElement.click();
});
}
@ -693,11 +693,11 @@ export class WebElementWrapper {
* @nonstandard
* @return {Promise<void>}
*/
public async scrollIntoViewIfNecessary(): Promise<void> {
public async scrollIntoViewIfNecessary(topOffset?: number): Promise<void> {
await this.driver.executeScript(
scrollIntoViewIfNecessary,
this._webElement,
this.fixedHeaderHeight
topOffset || this.fixedHeaderHeight
);
}

View file

@ -0,0 +1,142 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const a11y = getService('a11y');
const { common, detections } = getPageObjects(['common', 'detections']);
const security = getService('security');
const toasts = getService('toasts');
const testSubjects = getService('testSubjects');
describe('Security Solution', () => {
before(async () => {
await security.testUser.setRoles(['superuser'], false);
await common.navigateToApp('security');
});
after(async () => {
await security.testUser.restoreDefaults(false);
});
describe('Detections', () => {
describe('Create Rule Flow', () => {
beforeEach(async () => {
await detections.navigateToCreateRule();
});
describe('Custom Query Rule', () => {
describe('Define Step', () => {
it('default view meets a11y requirements', async () => {
await toasts.dismissAllToasts();
await testSubjects.click('customRuleType');
await a11y.testAppSnapshot();
});
describe('import query modal', () => {
it('contents of the default tab meets a11y requirements', async () => {
await detections.openImportQueryModal();
await a11y.testAppSnapshot();
});
it('contents of the templates tab meets a11y requirements', async () => {
await common.scrollKibanaBodyTop();
await detections.openImportQueryModal();
await detections.viewTemplatesInImportQueryModal();
await a11y.testAppSnapshot();
});
});
it('preview section meets a11y requirements', async () => {
await detections.addCustomQuery('_id');
await detections.preview();
await a11y.testAppSnapshot();
});
describe('About Step', () => {
beforeEach(async () => {
await detections.addCustomQuery('_id');
await detections.continue('define');
});
it('default view meets a11y requirements', async () => {
await a11y.testAppSnapshot();
});
it('advanced settings view meets a11y requirements', async () => {
await detections.revealAdvancedSettings();
await a11y.testAppSnapshot();
});
describe('Schedule Step', () => {
beforeEach(async () => {
await detections.addNameAndDescription();
await detections.continue('about');
});
it('meets a11y requirements', async () => {
await a11y.testAppSnapshot();
});
describe('Actions Step', () => {
it('meets a11y requirements', async () => {
await detections.continue('schedule');
await a11y.testAppSnapshot();
});
});
});
});
});
});
describe('Machine Learning Rule First Step', () => {
it('default view meets a11y requirements', async () => {
await detections.selectMLRule();
await a11y.testAppSnapshot();
});
});
describe('Threshold Rule Rule First Step', () => {
beforeEach(async () => {
await detections.selectThresholdRule();
});
it('default view meets a11y requirements', async () => {
await a11y.testAppSnapshot();
});
it('preview section meets a11y requirements', async () => {
await detections.addCustomQuery('_id');
await detections.preview();
await a11y.testAppSnapshot();
});
});
describe('Event Correlation Rule First Step', () => {
beforeEach(async () => {
await detections.selectEQLRule();
});
it('default view meets a11y requirements', async () => {
await a11y.testAppSnapshot();
});
});
describe('Indicator Match Rule First Step', () => {
beforeEach(async () => {
await detections.selectIndicatorMatchRule();
});
it('default view meets a11y requirements', async () => {
await a11y.testAppSnapshot();
});
});
});
});
});
}

View file

@ -34,6 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
require.resolve('./apps/lens'),
require.resolve('./apps/upgrade_assistant'),
require.resolve('./apps/canvas'),
require.resolve('./apps/security_solution'),
],
pageObjects,

View file

@ -197,6 +197,9 @@ export default async function ({ readConfigFile }) {
reporting: {
pathname: '/app/management/insightsAndAlerting/reporting',
},
securitySolution: {
pathname: '/app/security',
},
},
// choose where esArchiver should load archives from

View file

@ -40,6 +40,7 @@ import { IngestPipelinesPageProvider } from './ingest_pipelines_page';
import { TagManagementPageProvider } from './tag_management_page';
import { NavigationalSearchProvider } from './navigational_search';
import { SearchSessionsPageProvider } from './search_sessions_management_page';
import { DetectionsPageProvider } from '../../security_solution_ftr/page_objects/detections';
// just like services, PageObjects are defined as a map of
// names to Providers. Merge in Kibana's or pick specific ones
@ -77,4 +78,5 @@ export const pageObjects = {
roleMappings: RoleMappingsPageProvider,
ingestPipelines: IngestPipelinesPageProvider,
navigationalSearch: NavigationalSearchProvider,
detections: DetectionsPageProvider,
};

View file

@ -0,0 +1,149 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../../functional/ftr_provider_context';
import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper';
export function DetectionsPageProvider({ getService, getPageObjects }: FtrProviderContext) {
const find = getService('find');
const { common } = getPageObjects(['common']);
const testSubjects = getService('testSubjects');
class DetectionsPage {
async navigateHome(): Promise<void> {
await this.navigateToDetectionsPage();
}
async navigateToRules(): Promise<void> {
await this.navigateToDetectionsPage('rules');
}
async navigateToRuleMonitoring(): Promise<void> {
await common.clickAndValidate('allRulesTableTab-monitoring', 'monitoring-table');
}
async navigateToExceptionList(): Promise<void> {
await common.clickAndValidate('allRulesTableTab-exceptions', 'exceptions-table');
}
async navigateToCreateRule(): Promise<void> {
await this.navigateToDetectionsPage('rules/create');
}
async replaceIndexPattern(): Promise<void> {
const buttons = await find.allByCssSelector('[data-test-subj="comboBoxInput"] button');
await buttons.map(async (button: WebElementWrapper) => await button.click());
await testSubjects.setValue('comboBoxSearchInput', '*');
}
async openImportQueryModal(): Promise<void> {
const element = await testSubjects.find('importQueryFromSavedTimeline');
await element.click(500);
await testSubjects.exists('open-timeline-modal-body-filter-default');
}
async viewTemplatesInImportQueryModal(): Promise<void> {
await common.clickAndValidate('open-timeline-modal-body-filter-template', 'timelines-table');
}
async closeImportQueryModal(): Promise<void> {
await find.clickByCssSelector('.euiButtonIcon.euiButtonIcon--text.euiModal__closeIcon');
}
async selectMachineLearningJob(): Promise<void> {
await find.clickByCssSelector('[data-test-subj="mlJobSelect"] button');
await find.clickByCssSelector('#high_distinct_count_error_message');
}
async openAddFilterPopover(): Promise<void> {
const addButtons = await testSubjects.findAll('addFilter');
await addButtons[1].click();
await testSubjects.exists('saveFilter');
}
async closeAddFilterPopover(): Promise<void> {
await testSubjects.click('cancelSaveFilter');
}
async toggleFilterActions(): Promise<void> {
const filterActions = await testSubjects.findAll('addFilter');
await filterActions[1].click();
}
async toggleSavedQueries(): Promise<void> {
const filterActions = await find.allByCssSelector(
'[data-test-subj="saved-query-management-popover-button"]'
);
await filterActions[1].click();
}
async addNameAndDescription(
name: string = 'test rule name',
description: string = 'test rule description'
): Promise<void> {
await find.setValue(`[aria-describedby="detectionEngineStepAboutRuleName"]`, name, 500);
await find.setValue(
`[aria-describedby="detectionEngineStepAboutRuleDescription"]`,
description,
500
);
}
async goBackToAllRules(): Promise<void> {
await common.clickAndValidate('ruleDetailsBackToAllRules', 'create-new-rule');
}
async revealAdvancedSettings(): Promise<void> {
await common.clickAndValidate(
'advancedSettings',
'detectionEngineStepAboutRuleReferenceUrls'
);
}
async preview(): Promise<void> {
await common.clickAndValidate(
'queryPreviewButton',
'queryPreviewCustomHistogram',
undefined,
500
);
}
async continue(prefix: string): Promise<void> {
await testSubjects.click(`${prefix}-continue`);
}
async addCustomQuery(query: string): Promise<void> {
await testSubjects.setValue('queryInput', query, undefined, 500);
}
async selectMLRule(): Promise<void> {
await common.clickAndValidate('machineLearningRuleType', 'mlJobSelect');
}
async selectEQLRule(): Promise<void> {
await common.clickAndValidate('eqlRuleType', 'eqlQueryBarTextInput');
}
async selectIndicatorMatchRule(): Promise<void> {
await common.clickAndValidate('threatMatchRuleType', 'comboBoxInput');
}
async selectThresholdRule(): Promise<void> {
await common.clickAndValidate('thresholdRuleType', 'input');
}
private async navigateToDetectionsPage(path: string = ''): Promise<void> {
const subUrl = `detections${path ? `/${path}` : ''}`;
await common.navigateToUrl('securitySolution', subUrl, {
shouldUseHashForSubUrl: false,
});
}
}
return new DetectionsPage();
}