[Cases] Initial functional tests for cases in the stack management page (#127858)

* Adding data-test-subj to cases header component

* adding casesApp service for functional tests

* Adding test for create case

* Add more tests

* Add tests for cases list

* Update tests file structure

* Improve test structure

* Add cleanup methods

* Remove empty functions

* Use api to create case to edit

* move some repeated code to a service

* Unify casesapp provider in a single namespace

* Apply PR comment suggestions

* Remove .only from test suite

* Fix broken unit test

* Attempt to fix flaky test

* Another attempt to fix flaky test

* Move checks up for flaky tests

* increase timeout for flaky test

* Try to fix flaky test

* MOre fixes for flaky test

* rename cases app and fix nitpicks

* Rename variables

* fix more nits

* add more create case validatioons

* add more create and edit case validations

* Add extra validations to edit case

* Fix typo

* try to fix flaky test
This commit is contained in:
Esteban Beltran 2022-03-22 19:49:08 +01:00 committed by GitHub
parent 5d519f3e72
commit 0dc168e086
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 619 additions and 4 deletions

View file

@ -88,7 +88,7 @@ export const CasesTable: FunctionComponent<CasesTableProps> = ({
<EuiLoadingContent data-test-subj="initialLoadingPanelAllCases" lines={10} />
</Div>
) : (
<Div>
<Div data-test-subj={isCasesLoading ? 'cases-table-loading' : null}>
<CasesTableUtilityBar
data={data}
enableBulkActions={showActions}

View file

@ -62,6 +62,7 @@ export interface HeaderPageProps extends HeaderProps {
subtitle2?: SubtitleProps['items'];
title: string | React.ReactNode;
titleNode?: React.ReactElement;
'data-test-subj'?: string;
}
const HeaderPageComponent: React.FC<HeaderPageProps> = ({
@ -73,6 +74,7 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
subtitle2,
title,
titleNode,
'data-test-subj': dataTestSubj,
}) => {
const { releasePhase } = useCasesContext();
const { getAllCasesUrl, navigateToAllCases } = useAllCasesNavigation();
@ -88,7 +90,7 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
);
return (
<Header border={border}>
<Header border={border} data-test-subj={dataTestSubj}>
<EuiFlexGroup alignItems="center">
<FlexItem>
{showBackButton && (

View file

@ -46,15 +46,15 @@ Popover.displayName = 'Popover';
export interface UtilityBarActionProps extends LinkIconProps {
popoverContent?: (closePopover: () => void) => React.ReactNode;
dataTestSubj?: string;
ownFocus?: boolean;
dataTestSubj?: string;
}
export const UtilityBarAction = React.memo<UtilityBarActionProps>(
({
dataTestSubj,
children,
color,
dataTestSubj,
disabled,
href,
iconSide,

View file

@ -119,6 +119,9 @@ export default async function ({ readConfigFile }) {
logstashPipelines: {
pathname: '/app/management/ingest/pipelines',
},
cases: {
pathname: '/app/management/insightsAndAlerting/cases/',
},
maps: {
pathname: '/app/maps',
},

View file

@ -0,0 +1,45 @@
/*
* 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 pMap from 'p-map';
import { CasePostRequest } from '../../../../plugins/cases/common/api';
import { createCase, deleteAllCaseItems } from '../../../cases_api_integration/common/lib/utils';
import { FtrProviderContext } from '../../ftr_provider_context';
import { generateRandomCaseWithoutConnector } from './helpers';
export function CasesAPIServiceProvider({ getService }: FtrProviderContext) {
const kbnSupertest = getService('supertest');
const es = getService('es');
return {
async createCaseWithData(overwrites: { title?: string } = {}) {
const caseData = {
...generateRandomCaseWithoutConnector(),
...overwrites,
} as CasePostRequest;
await createCase(kbnSupertest, caseData);
},
async createNthRandomCases(amount: number = 3) {
const cases: CasePostRequest[] = Array.from(
{ length: amount },
() => generateRandomCaseWithoutConnector() as CasePostRequest
);
await pMap(
cases,
(caseData) => {
return createCase(kbnSupertest, caseData);
},
{ concurrency: 4 }
);
},
async deleteAllCases() {
deleteAllCaseItems(es);
},
};
}

View file

@ -0,0 +1,160 @@
/*
* 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 expect from '@kbn/expect';
import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper';
import uuid from 'uuid';
import { FtrProviderContext } from '../../ftr_provider_context';
export function CasesCommonServiceProvider({ getService, getPageObject }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const find = getService('find');
const comboBox = getService('comboBox');
const header = getPageObject('header');
return {
/**
* Opens the create case page pressing the "create case" button.
*
* Doesn't do navigation. Only works if you are already inside a cases app page.
* Does not work with the cases flyout.
*/
async openCreateCasePage() {
await testSubjects.click('createNewCaseBtn');
await testSubjects.existOrFail('create-case-submit', {
timeout: 5000,
});
},
/**
* it creates a new case from the create case page
* and leaves the navigation in the case view page
*
* Doesn't do navigation. Only works if you are already inside a cases app page.
* Does not work with the cases flyout.
*/
async createCaseFromCreateCasePage({
title = 'test-' + uuid.v4(),
description = 'desc' + uuid.v4(),
tag = 'tagme',
}: {
title: string;
description: string;
tag: string;
}) {
await this.openCreateCasePage();
// case name
await testSubjects.setValue('input', title);
// case tag
await comboBox.setCustom('comboBoxInput', tag);
// case description
const descriptionArea = await find.byCssSelector('textarea.euiMarkdownEditorTextArea');
await descriptionArea.focus();
await descriptionArea.type(description);
// save
await testSubjects.click('create-case-submit');
await testSubjects.existOrFail('case-view-title');
},
/**
* Goes to the first case listed on the table.
*
* This will fail if the table doesn't have any case
*/
async goToFirstListedCase() {
await testSubjects.existOrFail('cases-table');
await testSubjects.click('case-details-link');
await testSubjects.existOrFail('case-view-title');
},
/**
* Marks a case in progress via the status dropdown
*/
async markCaseInProgressViaDropdown() {
await this.openCaseSetStatusDropdown();
await testSubjects.click('case-view-status-dropdown-in-progress');
// wait for backend response
await testSubjects.existOrFail('header-page-supplements > status-badge-in-progress', {
timeout: 5000,
});
},
/**
* Marks a case closed via the status dropdown
*/
async markCaseClosedViaDropdown() {
this.openCaseSetStatusDropdown();
await testSubjects.click('case-view-status-dropdown-closed');
// wait for backend response
await testSubjects.existOrFail('header-page-supplements > status-badge-closed', {
timeout: 5000,
});
},
/**
* Marks a case open via the status dropdown
*/
async markCaseOpenViaDropdown() {
this.openCaseSetStatusDropdown();
await testSubjects.click('case-view-status-dropdown-open');
// wait for backend response
await testSubjects.existOrFail('header-page-supplements > status-badge-open', {
timeout: 5000,
});
},
async bulkDeleteAllCases() {
await testSubjects.setCheckbox('checkboxSelectAll', 'check');
const button = await find.byCssSelector('[aria-label="Bulk actions"]');
await button.click();
await testSubjects.click('cases-bulk-delete-button');
await testSubjects.click('confirmModalConfirmButton');
},
async selectAndDeleteAllCases() {
await header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('cases-table', { timeout: 20 * 1000 });
let rows: WebElementWrapper[];
do {
await header.waitUntilLoadingHasFinished();
await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 });
rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100);
if (rows.length > 0) {
await this.bulkDeleteAllCases();
// wait for a second
await new Promise((r) => setTimeout(r, 1000));
await header.waitUntilLoadingHasFinished();
}
} while (rows.length > 0);
},
async validateCasesTableHasNthRows(nrRows: number) {
await header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('cases-table', { timeout: 20 * 1000 });
await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 });
const rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100);
expect(rows.length).equal(nrRows);
},
async openCaseSetStatusDropdown() {
const button = await find.byCssSelector(
'[data-test-subj="case-view-status-dropdown"] button'
);
await button.click();
},
};
}

View file

@ -0,0 +1,26 @@
/*
* 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 uuid from 'uuid';
export function generateRandomCaseWithoutConnector() {
return {
title: 'random-' + uuid.v4(),
tags: ['test', uuid.v4()],
description: 'This is a description with id: ' + uuid.v4(),
connector: {
id: 'none',
name: 'none',
type: '.none',
fields: null,
},
settings: {
syncAlerts: false,
},
owner: 'cases',
};
}

View file

@ -0,0 +1,17 @@
/*
* 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';
import { CasesAPIServiceProvider } from './api';
import { CasesCommonServiceProvider } from './common';
export function CasesServiceProvider(context: FtrProviderContext) {
return {
api: CasesAPIServiceProvider(context),
common: CasesCommonServiceProvider(context),
};
}

View file

@ -69,6 +69,7 @@ import {
import { SearchSessionsService } from './search_sessions';
import { ObservabilityProvider } from './observability';
import { CompareImagesProvider } from './compare_images';
import { CasesServiceProvider } from './cases';
// define the name and providers for services that should be
// available to your tests. If you don't specify anything here
@ -128,4 +129,5 @@ export const services = {
searchSessions: SearchSessionsService,
observability: ObservabilityProvider,
compareImages: CompareImagesProvider,
cases: CasesServiceProvider,
};

View file

@ -0,0 +1,51 @@
/*
* 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 expect from '@kbn/expect';
import uuid from 'uuid';
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObject, getService }: FtrProviderContext) => {
describe('Create case', function () {
const common = getPageObject('common');
const find = getService('find');
const cases = getService('cases');
const testSubjects = getService('testSubjects');
before(async () => {
await common.navigateToApp('cases');
});
after(async () => {
await cases.api.deleteAllCases();
});
it('creates a case from the stack management page', async () => {
const caseTitle = 'test-' + uuid.v4();
await cases.common.createCaseFromCreateCasePage({
title: caseTitle,
description: 'test description',
tag: 'tagme',
});
// validate title
const title = await find.byCssSelector('[data-test-subj="header-page-title"]');
expect(await title.getVisibleText()).equal(caseTitle);
// validate description
const description = await testSubjects.find('user-action-markdown');
expect(await description.getVisibleText()).equal('test description');
// validate tag exists
await testSubjects.existOrFail('tag-tagme');
// validate no connector added
const button = await find.byCssSelector('[data-test-subj*="case-callout"] button');
expect(await button.getVisibleText()).equal('Add connector');
});
});
};

View file

@ -0,0 +1,169 @@
/*
* 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 expect from '@kbn/expect';
import uuid from 'uuid';
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObject, getService }: FtrProviderContext) => {
const common = getPageObject('common');
const header = getPageObject('header');
const testSubjects = getService('testSubjects');
const find = getService('find');
const cases = getService('cases');
const retry = getService('retry');
const comboBox = getService('comboBox');
describe('Edit case', () => {
// create the case to test on
before(async () => {
await common.navigateToApp('cases');
await cases.api.createNthRandomCases(1);
});
after(async () => {
await cases.api.deleteAllCases();
});
beforeEach(async () => {
await common.navigateToApp('cases');
await cases.common.goToFirstListedCase();
await header.waitUntilLoadingHasFinished();
});
it('edits a case title from the case view page', async () => {
const newTitle = `test-${uuid.v4()}`;
await testSubjects.click('editable-title-edit-icon');
await testSubjects.setValue('editable-title-input-field', newTitle);
await testSubjects.click('editable-title-submit-btn');
// wait for backend response
await retry.tryForTime(5000, async () => {
const title = await find.byCssSelector('[data-test-subj="header-page-title"]');
expect(await title.getVisibleText()).equal(newTitle);
});
// validate user action
await find.byCssSelector('[data-test-subj*="title-update-action"]');
});
it('adds a comment to a case', async () => {
const commentArea = await find.byCssSelector(
'[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea'
);
await commentArea.focus();
await commentArea.type('Test comment from automation');
await testSubjects.click('submit-comment');
// validate user action
const newComment = await find.byCssSelector(
'[data-test-subj*="comment-create-action"] [data-test-subj="user-action-markdown"]'
);
expect(await newComment.getVisibleText()).equal('Test comment from automation');
});
it('adds a tag to a case', async () => {
const tag = uuid.v4();
await testSubjects.click('tag-list-edit-button');
await comboBox.setCustom('comboBoxInput', tag);
await testSubjects.click('edit-tags-submit');
// validate tag was added
await testSubjects.existOrFail('tag-' + tag);
// validate user action
await find.byCssSelector('[data-test-subj*="tags-add-action"]');
});
it('deletes a tag from a case', async () => {
await testSubjects.click('tag-list-edit-button');
// find the tag button and click the close button
const button = await find.byCssSelector('[data-test-subj="comboBoxInput"] button');
await button.click();
await testSubjects.click('edit-tags-submit');
// validate user action
await find.byCssSelector('[data-test-subj*="tags-delete-action"]');
});
it('changes a case status to in-progress via dropdown menu', async () => {
await cases.common.markCaseInProgressViaDropdown();
// validate user action
await find.byCssSelector(
'[data-test-subj*="status-update-action"] [data-test-subj="status-badge-in-progress"]'
);
// validates dropdown tag
await testSubjects.existOrFail('case-view-status-dropdown > status-badge-in-progress');
});
it('changes a case status to closed via dropdown-menu', async () => {
await cases.common.markCaseClosedViaDropdown();
// validate user action
await find.byCssSelector(
'[data-test-subj*="status-update-action"] [data-test-subj="status-badge-closed"]'
);
// validates dropdown tag
await testSubjects.existOrFail('case-view-status-dropdown > status-badge-closed');
});
it("reopens a case from the 'reopen case' button", async () => {
await cases.common.markCaseClosedViaDropdown();
await header.waitUntilLoadingHasFinished();
await testSubjects.click('case-view-status-action-button');
await header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('header-page-supplements > status-badge-open', {
timeout: 5000,
});
// validate user action
await find.byCssSelector(
'[data-test-subj*="status-update-action"] [data-test-subj="status-badge-open"]'
);
// validates dropdown tag
await testSubjects.existOrFail('case-view-status-dropdown > status-badge-open');
});
it("marks in progress a case from the 'mark in progress' button", async () => {
await cases.common.markCaseOpenViaDropdown();
await header.waitUntilLoadingHasFinished();
await testSubjects.click('case-view-status-action-button');
await header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('header-page-supplements > status-badge-in-progress', {
timeout: 5000,
});
// validate user action
await find.byCssSelector(
'[data-test-subj*="status-update-action"] [data-test-subj="status-badge-in-progress"]'
);
// validates dropdown tag
await testSubjects.existOrFail('case-view-status-dropdown > status-badge-in-progress');
});
it("closes a case from the 'close case' button", async () => {
await cases.common.markCaseInProgressViaDropdown();
await header.waitUntilLoadingHasFinished();
await testSubjects.click('case-view-status-action-button');
await header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('header-page-supplements > status-badge-closed', {
timeout: 5000,
});
// validate user action
await find.byCssSelector(
'[data-test-subj*="status-update-action"] [data-test-subj="status-badge-closed"]'
);
// validates dropdown tag
await testSubjects.existOrFail('case-view-status-dropdown > status-badge-closed');
});
});
};

View file

@ -0,0 +1,17 @@
/*
* 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 ({ loadTestFile }: FtrProviderContext) => {
describe('Cases', function () {
this.tags('ciGroup27');
loadTestFile(require.resolve('./create_case_form'));
loadTestFile(require.resolve('./edit_case_form'));
loadTestFile(require.resolve('./list_view'));
});
};

View file

@ -0,0 +1,122 @@
/*
* 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 uuid from 'uuid';
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObject, getService }: FtrProviderContext) => {
const common = getPageObject('common');
const header = getPageObject('header');
const testSubjects = getService('testSubjects');
const cases = getService('cases');
const retry = getService('retry');
const browser = getService('browser');
describe('cases list', () => {
before(async () => {
await common.navigateToApp('cases');
await cases.api.deleteAllCases();
});
after(async () => {
await cases.api.deleteAllCases();
});
beforeEach(async () => {
await common.navigateToApp('cases');
});
it('displays an empty list with an add button correctly', async () => {
await testSubjects.existOrFail('cases-table-add-case');
});
it('lists cases correctly', async () => {
const NUMBER_CASES = 2;
await cases.api.createNthRandomCases(NUMBER_CASES);
await common.navigateToApp('cases');
await cases.common.validateCasesTableHasNthRows(NUMBER_CASES);
});
it('deletes a case correctly from the list', async () => {
await cases.api.createNthRandomCases(1);
await common.navigateToApp('cases');
await testSubjects.click('action-delete');
await testSubjects.click('confirmModalConfirmButton');
await testSubjects.existOrFail('euiToastHeader');
});
it('filters cases from the list with partial match', async () => {
await cases.api.deleteAllCases();
await cases.api.createNthRandomCases(5);
const id = uuid.v4();
const caseTitle = 'matchme-' + id;
await cases.api.createCaseWithData({ title: caseTitle });
await common.navigateToApp('cases');
await testSubjects.missingOrFail('cases-table-loading', { timeout: 5000 });
// search
const input = await testSubjects.find('search-cases');
await input.type(caseTitle);
await input.pressKeys(browser.keys.ENTER);
await retry.tryForTime(20000, async () => {
await cases.common.validateCasesTableHasNthRows(1);
});
});
it('paginates cases correctly', async () => {
await cases.api.deleteAllCases();
await cases.api.createNthRandomCases(8);
await common.navigateToApp('cases');
await testSubjects.click('tablePaginationPopoverButton');
await testSubjects.click('tablePagination-5-rows');
await testSubjects.isEnabled('pagination-button-1');
await testSubjects.click('pagination-button-1');
await testSubjects.isEnabled('pagination-button-0');
});
it('bulk delete cases from the list', async () => {
// deletes them from the API
await cases.api.deleteAllCases();
await cases.api.createNthRandomCases(8);
await common.navigateToApp('cases');
// deletes them from the UI
await cases.common.selectAndDeleteAllCases();
await cases.common.validateCasesTableHasNthRows(0);
});
describe('changes status from the list', () => {
before(async () => {
await common.navigateToApp('cases');
await cases.api.deleteAllCases();
await cases.api.createNthRandomCases(1);
await common.navigateToApp('cases');
});
it('to in progress', async () => {
await cases.common.openCaseSetStatusDropdown();
await testSubjects.click('case-view-status-dropdown-in-progress');
await header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('status-badge-in-progress');
});
it('to closed', async () => {
await cases.common.openCaseSetStatusDropdown();
await testSubjects.click('case-view-status-dropdown-closed');
await header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('status-badge-closed');
});
it('to open', async () => {
await cases.common.openCaseSetStatusDropdown();
await testSubjects.click('case-view-status-dropdown-open');
await header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('status-badge-open');
});
});
});
};

View file

@ -48,6 +48,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
resolve(__dirname, './apps/triggers_actions_ui'),
resolve(__dirname, './apps/uptime'),
resolve(__dirname, './apps/ml'),
resolve(__dirname, './apps/cases'),
],
apps: {
...xpackFunctionalConfig.get('apps'),