[ML] Adds functional tests for anomaly detection job custom URLs (#100455)

* [ML] Adds functional tests for anomaly detection job custom URLs

* [ML] Remove debug test tag from custom URL tests

* [ML] Update custom URL editor Jest snapshots

* [ML] Clean up in embeddables tests to fix dashboard test

* [ML] Delete test dashboard after test suites complete

* [ML] Edits to custom URL tests following review

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pete Harverson 2021-05-26 17:48:03 +01:00 committed by GitHub
parent c5f8aeeb43
commit f77ff2d396
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 621 additions and 15 deletions

View file

@ -18,6 +18,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe
/>
<EuiForm
className="ml-edit-url-form"
data-test-subj="mlJobCustomUrlForm"
>
<EuiFormRow
className="url-label"
@ -63,6 +64,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe
>
<EuiRadioGroup
className="url-link-to-radio"
data-test-subj="mlJobCustomUrlLinkToTypeInput"
idSelected="KIBANA_DASHBOARD"
onChange={[Function]}
options={
@ -100,6 +102,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe
>
<EuiSelect
compressed={true}
data-test-subj="mlJobCustomUrlDashboardNameInput"
onChange={[Function]}
options={
Array [
@ -134,6 +137,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe
<EuiComboBox
async={false}
compressed={false}
data-test-subj="mlJobCustomUrlQueryEntitiesInput"
fullWidth={false}
isClearable={true}
onChange={[Function]}
@ -175,6 +179,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe
>
<EuiSelect
compressed={true}
data-test-subj="mlJobCustomUrlTimeRangeInput"
onChange={[Function]}
options={
Array [
@ -215,6 +220,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with an enti
/>
<EuiForm
className="ml-edit-url-form"
data-test-subj="mlJobCustomUrlForm"
>
<EuiFormRow
className="url-label"
@ -260,6 +266,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with an enti
>
<EuiRadioGroup
className="url-link-to-radio"
data-test-subj="mlJobCustomUrlLinkToTypeInput"
idSelected="KIBANA_DISCOVER"
onChange={[Function]}
options={
@ -297,6 +304,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with an enti
>
<EuiSelect
compressed={true}
data-test-subj="mlJobCustomUrlDiscoverIndexPatternInput"
onChange={[Function]}
options={
Array [
@ -331,6 +339,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with an enti
<EuiComboBox
async={false}
compressed={false}
data-test-subj="mlJobCustomUrlQueryEntitiesInput"
fullWidth={false}
isClearable={true}
onChange={[Function]}
@ -378,6 +387,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with an enti
>
<EuiSelect
compressed={true}
data-test-subj="mlJobCustomUrlTimeRangeInput"
onChange={[Function]}
options={
Array [
@ -420,6 +430,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with an enti
>
<EuiFieldText
compressed={true}
data-test-subj="mlJobCustomUrlTimeRangeIntervalInput"
isInvalid={true}
onChange={[Function]}
value=""
@ -449,6 +460,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with valid t
/>
<EuiForm
className="ml-edit-url-form"
data-test-subj="mlJobCustomUrlForm"
>
<EuiFormRow
className="url-label"
@ -494,6 +506,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with valid t
>
<EuiRadioGroup
className="url-link-to-radio"
data-test-subj="mlJobCustomUrlLinkToTypeInput"
idSelected="KIBANA_DISCOVER"
onChange={[Function]}
options={
@ -531,6 +544,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with valid t
>
<EuiSelect
compressed={true}
data-test-subj="mlJobCustomUrlDiscoverIndexPatternInput"
onChange={[Function]}
options={
Array [
@ -565,6 +579,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with valid t
<EuiComboBox
async={false}
compressed={false}
data-test-subj="mlJobCustomUrlQueryEntitiesInput"
fullWidth={false}
isClearable={true}
onChange={[Function]}
@ -612,6 +627,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with valid t
>
<EuiSelect
compressed={true}
data-test-subj="mlJobCustomUrlTimeRangeInput"
onChange={[Function]}
options={
Array [
@ -650,6 +666,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with valid t
>
<EuiFieldText
compressed={true}
data-test-subj="mlJobCustomUrlTimeRangeIntervalInput"
isInvalid={false}
onChange={[Function]}
value="1h"
@ -679,6 +696,7 @@ exports[`CustomUrlEditor renders the editor for a new dashboard type URL with no
/>
<EuiForm
className="ml-edit-url-form"
data-test-subj="mlJobCustomUrlForm"
>
<EuiFormRow
className="url-label"
@ -728,6 +746,7 @@ exports[`CustomUrlEditor renders the editor for a new dashboard type URL with no
>
<EuiRadioGroup
className="url-link-to-radio"
data-test-subj="mlJobCustomUrlLinkToTypeInput"
idSelected="KIBANA_DASHBOARD"
onChange={[Function]}
options={
@ -765,6 +784,7 @@ exports[`CustomUrlEditor renders the editor for a new dashboard type URL with no
>
<EuiSelect
compressed={true}
data-test-subj="mlJobCustomUrlDashboardNameInput"
onChange={[Function]}
options={
Array [
@ -799,6 +819,7 @@ exports[`CustomUrlEditor renders the editor for a new dashboard type URL with no
<EuiComboBox
async={false}
compressed={false}
data-test-subj="mlJobCustomUrlQueryEntitiesInput"
fullWidth={false}
isClearable={true}
onChange={[Function]}
@ -840,6 +861,7 @@ exports[`CustomUrlEditor renders the editor for a new dashboard type URL with no
>
<EuiSelect
compressed={true}
data-test-subj="mlJobCustomUrlTimeRangeInput"
onChange={[Function]}
options={
Array [
@ -880,6 +902,7 @@ exports[`CustomUrlEditor renders the editor for other type of URL with duplicate
/>
<EuiForm
className="ml-edit-url-form"
data-test-subj="mlJobCustomUrlForm"
>
<EuiFormRow
className="url-label"
@ -929,6 +952,7 @@ exports[`CustomUrlEditor renders the editor for other type of URL with duplicate
>
<EuiRadioGroup
className="url-link-to-radio"
data-test-subj="mlJobCustomUrlLinkToTypeInput"
idSelected="OTHER"
onChange={[Function]}
options={
@ -966,6 +990,7 @@ exports[`CustomUrlEditor renders the editor for other type of URL with duplicate
>
<EuiTextArea
compressed={true}
data-test-subj="mlJobCustomUrlOtherTypeUrlInput"
fullWidth={true}
onChange={[Function]}
rows={2}
@ -994,6 +1019,7 @@ exports[`CustomUrlEditor renders the editor for other type of URL with unique la
/>
<EuiForm
className="ml-edit-url-form"
data-test-subj="mlJobCustomUrlForm"
>
<EuiFormRow
className="url-label"
@ -1039,6 +1065,7 @@ exports[`CustomUrlEditor renders the editor for other type of URL with unique la
>
<EuiRadioGroup
className="url-link-to-radio"
data-test-subj="mlJobCustomUrlLinkToTypeInput"
idSelected="OTHER"
onChange={[Function]}
options={
@ -1076,6 +1103,7 @@ exports[`CustomUrlEditor renders the editor for other type of URL with unique la
>
<EuiTextArea
compressed={true}
data-test-subj="mlJobCustomUrlOtherTypeUrlInput"
fullWidth={true}
onChange={[Function]}
rows={2}

View file

@ -12,6 +12,7 @@ exports[`CustomUrlList renders a list of custom URLs 1`] = `
grow={false}
>
<EuiFormRow
data-test-subj="mlJobEditCustomUrlItemLabel"
describedByIds={Array []}
display="row"
error={Array []}
@ -114,6 +115,7 @@ exports[`CustomUrlList renders a list of custom URLs 1`] = `
<EuiButtonIcon
aria-label="Test custom URL"
color="primary"
data-test-subj="mlJobEditTestCustomUrlButton"
iconType="popout"
onClick={[Function]}
size="s"
@ -146,6 +148,7 @@ exports[`CustomUrlList renders a list of custom URLs 1`] = `
<EuiButtonIcon
aria-label="Delete custom URL"
color="danger"
data-test-subj="mlJobEditDeleteCustomUrlButton_0"
iconType="trash"
onClick={[Function]}
size="s"
@ -162,6 +165,7 @@ exports[`CustomUrlList renders a list of custom URLs 1`] = `
grow={false}
>
<EuiFormRow
data-test-subj="mlJobEditCustomUrlItemLabel"
describedByIds={Array []}
display="row"
error={Array []}
@ -264,6 +268,7 @@ exports[`CustomUrlList renders a list of custom URLs 1`] = `
<EuiButtonIcon
aria-label="Test custom URL"
color="primary"
data-test-subj="mlJobEditTestCustomUrlButton"
iconType="popout"
onClick={[Function]}
size="s"
@ -296,6 +301,7 @@ exports[`CustomUrlList renders a list of custom URLs 1`] = `
<EuiButtonIcon
aria-label="Delete custom URL"
color="danger"
data-test-subj="mlJobEditDeleteCustomUrlButton_1"
iconType="trash"
onClick={[Function]}
size="s"
@ -312,6 +318,7 @@ exports[`CustomUrlList renders a list of custom URLs 1`] = `
grow={false}
>
<EuiFormRow
data-test-subj="mlJobEditCustomUrlItemLabel"
describedByIds={Array []}
display="row"
error={Array []}
@ -414,6 +421,7 @@ exports[`CustomUrlList renders a list of custom URLs 1`] = `
<EuiButtonIcon
aria-label="Test custom URL"
color="primary"
data-test-subj="mlJobEditTestCustomUrlButton"
iconType="popout"
onClick={[Function]}
size="s"
@ -446,6 +454,7 @@ exports[`CustomUrlList renders a list of custom URLs 1`] = `
<EuiButtonIcon
aria-label="Delete custom URL"
color="danger"
data-test-subj="mlJobEditDeleteCustomUrlButton_2"
iconType="trash"
onClick={[Function]}
size="s"

View file

@ -209,7 +209,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
</h4>
</EuiTitle>
<EuiSpacer size="m" />
<EuiForm className="ml-edit-url-form">
<EuiForm className="ml-edit-url-form" data-test-subj="mlJobCustomUrlForm">
<EuiFormRow
label={
<FormattedMessage id="xpack.ml.customUrlsEditor.labelLabel" defaultMessage="Label" />
@ -239,6 +239,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
idSelected={type}
onChange={onTypeChange}
className="url-link-to-radio"
data-test-subj="mlJobCustomUrlLinkToTypeInput"
/>
</EuiFormRow>
@ -256,6 +257,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
options={dashboardOptions}
value={kibanaSettings.dashboardId}
onChange={onDashboardChange}
data-test-subj="mlJobCustomUrlDashboardNameInput"
compressed
/>
</EuiFormRow>
@ -275,6 +277,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
options={indexPatternOptions}
value={kibanaSettings.discoverIndexPatternId}
onChange={onDiscoverIndexPatternChange}
data-test-subj="mlJobCustomUrlDiscoverIndexPatternInput"
compressed
/>
</EuiFormRow>
@ -298,6 +301,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
selectedOptions={selectedEntityOptions}
onChange={onQueryEntitiesChange}
isClearable={true}
data-test-subj="mlJobCustomUrlQueryEntitiesInput"
/>
</EuiFormRow>
)}
@ -321,6 +325,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
options={timeRangeOptions}
value={timeRange.type}
onChange={onTimeRangeTypeChange}
data-test-subj="mlJobCustomUrlTimeRangeInput"
compressed
/>
</EuiFormRow>
@ -343,6 +348,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
value={timeRange.interval}
onChange={onTimeRangeIntervalChange}
isInvalid={isInvalidTimeRange}
data-test-subj="mlJobCustomUrlTimeRangeIntervalInput"
compressed
/>
</EuiFormRow>
@ -365,6 +371,7 @@ export const CustomUrlEditor: FC<CustomUrlEditorProps> = ({
rows={2}
value={otherUrlSettings.urlValue}
onChange={onOtherUrlValueChange}
data-test-subj="mlJobCustomUrlOtherTypeUrlInput"
compressed
/>
</EuiFormRow>

View file

@ -160,6 +160,7 @@ export const CustomUrlList: FC<CustomUrlListProps> = ({ job, customUrls, setCust
}
isInvalid={isInvalidLabel}
error={invalidLabelError}
data-test-subj="mlJobEditCustomUrlItemLabel"
>
<EuiFieldText
value={label}
@ -239,6 +240,7 @@ export const CustomUrlList: FC<CustomUrlListProps> = ({ job, customUrls, setCust
aria-label={i18n.translate('xpack.ml.customUrlEditorList.testCustomUrlAriaLabel', {
defaultMessage: 'Test custom URL',
})}
data-test-subj="mlJobEditTestCustomUrlButton"
/>
</EuiToolTip>
</EuiFormRow>
@ -264,6 +266,7 @@ export const CustomUrlList: FC<CustomUrlListProps> = ({ job, customUrls, setCust
defaultMessage: 'Delete custom URL',
}
)}
data-test-subj={`mlJobEditDeleteCustomUrlButton_${index}`}
/>
</EuiToolTip>
</EuiFormRow>

View file

@ -326,6 +326,7 @@ export class EditJobFlyoutUI extends Component {
const tabs = [
{
id: 'job-details',
'data-test-subj': 'mlEditJobFlyout-jobDetails',
name: i18n.translate('xpack.ml.jobsList.editJobFlyout.jobDetailsTitle', {
defaultMessage: 'Job details',
}),
@ -346,6 +347,7 @@ export class EditJobFlyoutUI extends Component {
},
{
id: 'detectors',
'data-test-subj': 'mlEditJobFlyout-detectors',
name: i18n.translate('xpack.ml.jobsList.editJobFlyout.detectorsTitle', {
defaultMessage: 'Detectors',
}),
@ -359,6 +361,7 @@ export class EditJobFlyoutUI extends Component {
},
{
id: 'datafeed',
'data-test-subj': 'mlEditJobFlyout-datafeed',
name: i18n.translate('xpack.ml.jobsList.editJobFlyout.datafeedTitle', {
defaultMessage: 'Datafeed',
}),
@ -376,6 +379,7 @@ export class EditJobFlyoutUI extends Component {
},
{
id: 'custom-urls',
'data-test-subj': 'mlEditJobFlyout-customUrls',
name: i18n.translate('xpack.ml.jobsList.editJobFlyout.customUrlsTitle', {
defaultMessage: 'Custom URLs',
}),
@ -395,6 +399,7 @@ export class EditJobFlyoutUI extends Component {
this.closeFlyout();
}}
size="m"
data-test-subj="mlJobEditFlyout"
>
<EuiFlyoutHeader>
<EuiTitle>
@ -419,6 +424,7 @@ export class EditJobFlyoutUI extends Component {
this.closeFlyout();
}}
flush="left"
data-test-subj="mlEditJobFlyoutCloseButton"
>
<FormattedMessage
id="xpack.ml.jobsList.editJobFlyout.closeButtonLabel"
@ -431,6 +437,7 @@ export class EditJobFlyoutUI extends Component {
onClick={this.save}
fill
isDisabled={isValidJobDetails === false || isValidJobCustomUrls === false}
data-test-subj="mlEditJobFlyoutSaveButton"
>
<FormattedMessage
id="xpack.ml.jobsList.editJobFlyout.saveButtonLabel"

View file

@ -613,7 +613,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen();
await ml.testExecution.logTestStep('job cloning persists custom urls');
await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard');
await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard');
await ml.testExecution.logTestStep('job cloning persists assigned calendars');
await ml.jobWizardCommon.assertCalendarsSelection([calendarId]);

View file

@ -74,6 +74,10 @@ export default function ({ getService }: FtrProviderContext) {
await ml.securityUI.loginAsMlPowerUser();
});
after(async () => {
await ml.testResources.deleteMLTestDashboard();
});
for (const testData of testDataList) {
describe(testData.suiteSuffix, function () {
before(async () => {

View file

@ -278,7 +278,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen();
await ml.testExecution.logTestStep('job cloning persists custom urls');
await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard');
await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard');
await ml.testExecution.logTestStep('job cloning persists assigned calendars');
await ml.jobWizardCommon.assertCalendarsSelection([calendarId]);

View file

@ -0,0 +1,188 @@
/*
* 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 { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs';
import {
TimeRangeType,
TIME_RANGE_TYPE,
} from '../../../../../plugins/ml/public/application/jobs/components/custom_url_editor/constants';
interface DiscoverUrlConfig {
label: string;
indexPattern: string;
queryEntityFieldNames: string[];
timeRange: TimeRangeType;
timeRangeInterval?: string;
}
interface DashboardUrlConfig {
label: string;
dashboardName: string;
queryEntityFieldNames: string[];
timeRange: TimeRangeType;
timeRangeInterval?: string;
}
interface OtherUrlConfig {
label: string;
url: string;
}
// @ts-expect-error doesn't implement the full interface
const JOB_CONFIG: Job = {
job_id: `fq_multi_1_custom_urls`,
description: 'mean(responsetime) partition=airline on farequote dataset with 30m bucket span',
groups: ['farequote', 'automated', 'multi-metric'],
analysis_config: {
bucket_span: '30m',
influencers: ['airline'],
detectors: [{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }],
},
data_description: { time_field: '@timestamp' },
analysis_limits: { model_memory_limit: '20mb' },
model_plot_config: { enabled: true },
};
// @ts-expect-error doesn't implement the full interface
const DATAFEED_CONFIG: Datafeed = {
datafeed_id: 'datafeed-fq_multi_1_custom_urls',
indices: ['ft_farequote'],
job_id: 'fq_multi_1_custom_urls',
query: { bool: { must: [{ match_all: {} }] } },
};
const testDiscoverCustomUrl: DiscoverUrlConfig = {
label: 'Show data',
indexPattern: 'ft_farequote',
queryEntityFieldNames: ['airline'],
timeRange: TIME_RANGE_TYPE.AUTO,
};
const testDashboardCustomUrl: DashboardUrlConfig = {
label: 'Show dashboard',
dashboardName: 'ML Test',
queryEntityFieldNames: [],
timeRange: TIME_RANGE_TYPE.INTERVAL,
timeRangeInterval: '1h',
};
const testOtherCustomUrl: OtherUrlConfig = {
label: 'elastic.co',
url: 'https://www.elastic.co/',
};
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
const browser = getService('browser');
describe('custom urls', function () {
this.tags(['mlqa']);
before(async () => {
await esArchiver.loadIfNeeded('ml/farequote');
await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp');
await ml.testResources.createMLTestDashboardIfNeeded();
await ml.testResources.setKibanaTimeZoneToUTC();
await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG);
await ml.securityUI.loginAsMlPowerUser();
});
after(async () => {
await ml.testResources.deleteMLTestDashboard();
await ml.api.cleanMlIndices();
});
it('opens the custom URLs tab in the edit job flyout', async () => {
await ml.testExecution.logTestStep('load the job management page');
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
await ml.testExecution.logTestStep('open the custom URLs tab in the edit job flyout');
await ml.jobTable.openEditCustomUrlsForJobTab(JOB_CONFIG.job_id);
await ml.jobTable.closeEditJobFlyout();
});
it('adds a custom URL with query entities to Discover in the edit job flyout', async () => {
await ml.jobTable.addDiscoverCustomUrl(JOB_CONFIG.job_id, testDiscoverCustomUrl);
});
it('adds a custom URL to Dashboard in the edit job flyout', async () => {
await ml.jobTable.addDashboardCustomUrl(JOB_CONFIG.job_id, testDashboardCustomUrl);
});
it('adds a custom URL to an external page in the edit job flyout', async () => {
await ml.jobTable.addOtherTypeCustomUrl(JOB_CONFIG.job_id, testOtherCustomUrl);
});
it('tests other type custom URL', async () => {
await ml.jobTable.testOtherTypeCustomUrlAction(JOB_CONFIG.job_id, 2, testOtherCustomUrl.url);
});
it('edits other type custom URL', async () => {
const edit = {
label: `${testOtherCustomUrl.url} edited`,
url: `${testOtherCustomUrl.url}guide/index.html`,
};
await ml.testExecution.logTestStep('edit the custom URL in the edit job flyout');
await ml.jobTable.editCustomUrl(JOB_CONFIG.job_id, 2, edit);
await ml.testExecution.logTestStep('tests custom URL edit has been applied');
await ml.jobTable.testOtherTypeCustomUrlAction(JOB_CONFIG.job_id, 2, edit.url);
await ml.jobTable.closeEditJobFlyout();
});
it('deletes a custom URL', async () => {
await ml.jobTable.deleteCustomUrl(JOB_CONFIG.job_id, 2);
});
// wrapping into own describe to make sure new tab is cleaned up even if test failed
// see: https://github.com/elastic/kibana/pull/67280#discussion_r430528122
describe('tests Discover type custom URL', () => {
let tabsCount = 1;
const docCountFormatted = '268';
it('opens Discover page from test link in the edit job flyout', async () => {
await ml.jobTable.openTestCustomUrl(JOB_CONFIG.job_id, 0);
await browser.switchTab(1);
tabsCount++;
await ml.jobTable.testDiscoverCustomUrlAction(docCountFormatted);
});
after(async () => {
if (tabsCount > 1) {
await browser.closeCurrentWindow();
await browser.switchTab(0);
await ml.jobTable.closeEditJobFlyout();
}
});
});
// wrapping into own describe to make sure new tab is cleaned up even if test failed
// see: https://github.com/elastic/kibana/pull/67280#discussion_r430528122
describe('tests Dashboard type custom URL', () => {
let tabsCount = 1;
const testDashboardPanelCount = 0; // ML Test dashboard has no content.
it('opens Dashboard page from test link in the edit job flyout', async () => {
await ml.jobTable.openTestCustomUrl(JOB_CONFIG.job_id, 1);
await browser.switchTab(1);
tabsCount++;
await ml.jobTable.testDashboardCustomUrlAction(testDashboardPanelCount);
});
after(async () => {
if (tabsCount > 1) {
await browser.closeCurrentWindow();
await browser.switchTab(0);
await ml.jobTable.closeEditJobFlyout();
}
});
});
});
}

View file

@ -23,5 +23,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./date_nanos_job'));
loadTestFile(require.resolve('./annotations'));
loadTestFile(require.resolve('./aggregated_scripted_job'));
loadTestFile(require.resolve('./custom_urls'));
});
}

View file

@ -299,7 +299,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen();
await ml.testExecution.logTestStep('job cloning persists custom urls');
await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard');
await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard');
await ml.testExecution.logTestStep('job cloning persists assigned calendars');
await ml.jobWizardCommon.assertCalendarsSelection([calendarId]);

View file

@ -336,7 +336,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen();
await ml.testExecution.logTestStep('job cloning persists custom urls');
await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard');
await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard');
await ml.testExecution.logTestStep('job cloning persists assigned calendars');
await ml.jobWizardCommon.assertCalendarsSelection([calendarId]);

View file

@ -262,7 +262,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen();
await ml.testExecution.logTestStep('job cloning persists custom urls');
await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard');
await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard');
await ml.testExecution.logTestStep('job cloning persists assigned calendars');
await ml.jobWizardCommon.assertCalendarsSelection([calendarId]);

View file

@ -123,6 +123,25 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte
await this.assertRadioGroupValue(testSubject, value);
},
async assertSelectSelectedOptionVisibleText(testSubject: string, visibleText: string) {
// Need to validate the selected option text, as the option value may be different to the visible text.
const selectControl = await testSubjects.find(testSubject);
const selectedValue = await selectControl.getAttribute('value');
const selectedOption = await selectControl.findByCssSelector(`[value="${selectedValue}"]`);
const selectedOptionText = await selectedOption.getVisibleText();
expect(selectedOptionText).to.eql(
visibleText,
`Expected selected option visible text to be '${visibleText}' (got '${selectedOptionText}')`
);
},
async selectSelectValueByVisibleText(testSubject: string, visibleText: string) {
// Cannot use await testSubjects.selectValue as the option value may be different to the text.
const selectControl = await testSubjects.find(testSubject);
await selectControl.type(visibleText);
await this.assertSelectSelectedOptionVisibleText(testSubject, visibleText);
},
async setMultiSelectFilter(testDataSubj: string, fieldTypes: string[]) {
await testSubjects.clickWhenNotDisabled(`${testDataSubj}-button`);
await testSubjects.existOrFail(`${testDataSubj}-popover`);

View file

@ -12,10 +12,25 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export type MlCustomUrls = ProvidedType<typeof MachineLearningCustomUrlsProvider>;
export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderContext) {
export function MachineLearningCustomUrlsProvider({
getService,
getPageObjects,
}: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const comboBox = getService('comboBox');
const PageObjects = getPageObjects(['dashboard', 'discover', 'header']);
return {
async assertCustomUrlsLength(expectedLength: number) {
const customUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel');
const actualLength = customUrls.length;
expect(expectedLength).to.eql(
actualLength,
`Expected number of custom urls to be '${expectedLength}' (got '${actualLength}')`
);
},
async assertCustomUrlLabelValue(expectedValue: string) {
const actualCustomUrlLabel = await testSubjects.getAttribute(
'mlJobCustomUrlLabelInput',
@ -27,15 +42,68 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon
);
},
async setCustomUrlLabel(customUrlsLabel: string) {
await testSubjects.setValue('mlJobCustomUrlLabelInput', customUrlsLabel, {
async setCustomUrlLabel(customUrlLabel: string) {
await testSubjects.setValue('mlJobCustomUrlLabelInput', customUrlLabel, {
clearWithKeyboard: true,
});
await this.assertCustomUrlLabelValue(customUrlsLabel);
await this.assertCustomUrlLabelValue(customUrlLabel);
},
async assertCustomUrlItem(index: number, expectedLabel: string) {
await testSubjects.existOrFail(`mlJobEditCustomUrlItem_${index}`);
async assertCustomUrlQueryEntitySelection(expectedFieldNames: string[]) {
const actualFieldNames = await comboBox.getComboBoxSelectedOptions(
'mlJobCustomUrlQueryEntitiesInput > comboBoxInput'
);
expect(actualFieldNames).to.eql(
expectedFieldNames,
`Expected query entity selection to be '${expectedFieldNames}' (got '${actualFieldNames}')`
);
},
async setCustomUrlQueryEntityFieldNames(fieldNames: string[]) {
for (const fieldName of fieldNames) {
await comboBox.set('mlJobCustomUrlQueryEntitiesInput > comboBoxInput', fieldName);
}
await this.assertCustomUrlQueryEntitySelection(fieldNames);
},
async assertCustomUrlTimeRangeIntervalValue(expectedInterval: string) {
const actualCustomUrlTimeRangeInterval = await testSubjects.getAttribute(
'mlJobCustomUrlTimeRangeIntervalInput',
'value'
);
expect(actualCustomUrlTimeRangeInterval).to.eql(
expectedInterval,
`Expected custom url time range interval to be '${expectedInterval}' (got '${actualCustomUrlTimeRangeInterval}')`
);
},
async setCustomUrlTimeRangeInterval(interval: string) {
await testSubjects.setValue('mlJobCustomUrlTimeRangeIntervalInput', interval, {
clearWithKeyboard: true,
});
await this.assertCustomUrlTimeRangeIntervalValue(interval);
},
async assertCustomUrlOtherTypeUrlValue(expectedUrl: string) {
const actualCustomUrlValue = await testSubjects.getAttribute(
'mlJobCustomUrlOtherTypeUrlInput',
'value'
);
expect(actualCustomUrlValue).to.eql(
expectedUrl,
`Expected other type custom url value to be '${expectedUrl}' (got '${actualCustomUrlValue}')`
);
},
async setCustomUrlOtherTypeUrl(url: string) {
await testSubjects.setValue('mlJobCustomUrlOtherTypeUrlInput', url, {
clearWithKeyboard: true,
});
await this.assertCustomUrlOtherTypeUrlValue(url);
},
async assertCustomUrlLabel(index: number, expectedLabel: string) {
await testSubjects.existOrFail(`mlJobEditCustomUrlLabelInput_${index}`);
const actualLabel = await testSubjects.getAttribute(
`mlJobEditCustomUrlLabelInput_${index}`,
'value'
@ -46,6 +114,44 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon
);
},
async assertCustomUrlUrlValue(index: number, expectedUrl: string) {
await testSubjects.existOrFail(`mlJobEditCustomUrlInput_${index}`);
const actualUrl = await testSubjects.getAttribute(
`mlJobEditCustomUrlInput_${index}`,
'value'
);
expect(actualUrl).to.eql(
expectedUrl,
`Expected custom url item to be '${expectedUrl}' (got '${actualUrl}')`
);
},
async editCustomUrlLabel(index: number, label: string) {
await testSubjects.existOrFail(`mlJobEditCustomUrlLabelInput_${index}`);
await testSubjects.setValue(`mlJobEditCustomUrlLabelInput_${index}`, label, {
clearWithKeyboard: true,
});
await this.assertCustomUrlLabel(index, label);
},
async editCustomUrlUrlValue(index: number, urlValue: string) {
await testSubjects.existOrFail(`mlJobEditCustomUrlInput_${index}`);
await testSubjects.setValue(`mlJobEditCustomUrlInput_${index}`, urlValue, {
clearWithKeyboard: true,
});
// Click away, so the textarea reverts back to the standard input.
await testSubjects.click(`mlJobEditCustomUrlLabelInput_${index}`);
await this.assertCustomUrlUrlValue(index, urlValue);
},
async deleteCustomUrl(index: number) {
await testSubjects.existOrFail(`mlJobEditDeleteCustomUrlButton_${index}`);
const beforeCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel');
await testSubjects.click(`mlJobEditDeleteCustomUrlButton_${index}`);
await this.assertCustomUrlsLength(beforeCustomUrls.length - 1);
},
/**
* Submits the custom url form and adds it to the list.
* @param formContainerSelector - selector for the element that wraps the custom url creation form.
@ -54,5 +160,33 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon
await testSubjects.click('mlJobAddCustomUrl');
await testSubjects.missingOrFail(formContainerSelector, { timeout: 10 * 1000 });
},
async clickTestCustomUrl(index: number) {
await testSubjects.existOrFail(`mlJobEditCustomUrlItem_${index}`);
await testSubjects.click(`mlJobEditCustomUrlItem_${index} > mlJobEditTestCustomUrlButton`);
await PageObjects.header.waitUntilLoadingHasFinished();
},
async assertDiscoverCustomUrlAction(expectedHitCountFormatted: string) {
await PageObjects.discover.waitForDiscoverAppOnScreen();
await retry.tryForTime(5000, async () => {
const hitCount = await PageObjects.discover.getHitCount();
expect(hitCount).to.eql(
expectedHitCountFormatted,
`Expected Discover hit count to be '${expectedHitCountFormatted}' (got '${hitCount}')`
);
});
},
async assertDashboardCustomUrlAction(expectedPanelCount: number) {
await PageObjects.dashboard.waitForRenderComplete();
await retry.tryForTime(5000, async () => {
const panelCount = await PageObjects.dashboard.getPanelCount();
expect(panelCount).to.eql(
expectedPanelCount,
`Expected Dashboard panel count to be '${expectedPanelCount}' (got '${panelCount}')`
);
});
},
};
}

View file

@ -90,7 +90,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
const jobManagement = MachineLearningJobManagementProvider(context, api);
const jobSelection = MachineLearningJobSelectionProvider(context);
const jobSourceSelection = MachineLearningJobSourceSelectionProvider(context);
const jobTable = MachineLearningJobTableProvider(context);
const jobTable = MachineLearningJobTableProvider(context, commonUI, customUrls);
const jobTypeSelection = MachineLearningJobTypeSelectionProvider(context);
const jobWizardAdvanced = MachineLearningJobWizardAdvancedProvider(context, commonUI);
const jobWizardCategorization = MachineLearningJobWizardCategorizationProvider(context);

View file

@ -8,8 +8,20 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { MlCommonUI } from './common_ui';
import { MlCustomUrls } from './custom_urls';
export function MachineLearningJobTableProvider({ getService }: FtrProviderContext) {
import {
TimeRangeType,
TIME_RANGE_TYPE,
URL_TYPE,
} from '../../../../plugins/ml/public/application/jobs/components/custom_url_editor/constants';
export function MachineLearningJobTableProvider(
{ getService }: FtrProviderContext,
mlCommonUI: MlCommonUI,
customUrls: MlCustomUrls
) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
@ -311,6 +323,12 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte
await testSubjects.existOrFail('~mlPageJobWizard');
}
public async clickEditJobAction(jobId: string) {
await this.ensureJobActionsMenuOpen(jobId);
await testSubjects.click('mlActionButtonEditJob');
await testSubjects.existOrFail('mlJobEditFlyout');
}
public async clickDeleteJobAction(jobId: string) {
await this.ensureJobActionsMenuOpen(jobId);
await testSubjects.click('mlActionButtonDeleteJob');
@ -456,5 +474,189 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte
}
});
}
public async openEditCustomUrlsForJobTab(jobId: string) {
await this.clickEditJobAction(jobId);
// click Custom URLs tab
await testSubjects.click('mlEditJobFlyout-customUrls');
await this.ensureEditCustomUrlTabOpen();
}
public async ensureEditCustomUrlTabOpen() {
await testSubjects.existOrFail('mlJobOpenCustomUrlFormButton', { timeout: 5000 });
}
public async closeEditJobFlyout() {
if (await testSubjects.exists('mlEditJobFlyoutCloseButton')) {
await testSubjects.click('mlEditJobFlyoutCloseButton');
await testSubjects.missingOrFail('mlJobEditFlyout');
}
}
public async saveEditJobFlyoutChanges() {
await testSubjects.click('mlEditJobFlyoutSaveButton');
await testSubjects.missingOrFail('mlJobEditFlyout', { timeout: 5000 });
}
public async clickOpenCustomUrlEditor() {
await this.ensureEditCustomUrlTabOpen();
await testSubjects.click('mlJobOpenCustomUrlFormButton');
await testSubjects.existOrFail('mlJobCustomUrlForm');
}
public async addDiscoverCustomUrl(
jobId: string,
customUrl: {
label: string;
indexPattern: string;
queryEntityFieldNames: string[];
timeRange: TimeRangeType;
timeRangeInterval?: string;
}
) {
await this.openEditCustomUrlsForJobTab(jobId);
const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel');
// Fill-in the form
await this.clickOpenCustomUrlEditor();
await customUrls.setCustomUrlLabel(customUrl.label);
await mlCommonUI.selectRadioGroupValue(
`mlJobCustomUrlLinkToTypeInput`,
URL_TYPE.KIBANA_DISCOVER
);
await mlCommonUI.selectSelectValueByVisibleText(
'mlJobCustomUrlDiscoverIndexPatternInput',
customUrl.indexPattern
);
await customUrls.setCustomUrlQueryEntityFieldNames(customUrl.queryEntityFieldNames);
await mlCommonUI.selectSelectValueByVisibleText(
'mlJobCustomUrlTimeRangeInput',
customUrl.timeRange
);
if (customUrl.timeRange === TIME_RANGE_TYPE.INTERVAL) {
await customUrls.setCustomUrlTimeRangeInterval(customUrl.timeRangeInterval!);
}
// Save custom URL
await testSubjects.click('mlJobAddCustomUrl');
const expectedIndex = existingCustomUrls.length;
await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label);
// Save the job
await this.saveEditJobFlyoutChanges();
}
public async addDashboardCustomUrl(
jobId: string,
customUrl: {
label: string;
dashboardName: string;
queryEntityFieldNames: string[];
timeRange: TimeRangeType;
timeRangeInterval?: string;
}
) {
await this.openEditCustomUrlsForJobTab(jobId);
const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel');
// Fill-in the form
await this.clickOpenCustomUrlEditor();
await customUrls.setCustomUrlLabel(customUrl.label);
await mlCommonUI.selectRadioGroupValue(
`mlJobCustomUrlLinkToTypeInput`,
URL_TYPE.KIBANA_DASHBOARD
);
await mlCommonUI.selectSelectValueByVisibleText(
'mlJobCustomUrlDashboardNameInput',
customUrl.dashboardName
);
await customUrls.setCustomUrlQueryEntityFieldNames(customUrl.queryEntityFieldNames);
await mlCommonUI.selectSelectValueByVisibleText(
'mlJobCustomUrlTimeRangeInput',
customUrl.timeRange
);
if (customUrl.timeRange === TIME_RANGE_TYPE.INTERVAL) {
await customUrls.setCustomUrlTimeRangeInterval(customUrl.timeRangeInterval!);
}
// Save custom URL
await testSubjects.click('mlJobAddCustomUrl');
const expectedIndex = existingCustomUrls.length;
await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label);
// Save the job
await this.saveEditJobFlyoutChanges();
}
public async addOtherTypeCustomUrl(jobId: string, customUrl: { label: string; url: string }) {
await this.openEditCustomUrlsForJobTab(jobId);
const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel');
// Fill-in the form
await this.clickOpenCustomUrlEditor();
await customUrls.setCustomUrlLabel(customUrl.label);
await mlCommonUI.selectRadioGroupValue(`mlJobCustomUrlLinkToTypeInput`, URL_TYPE.OTHER);
await customUrls.setCustomUrlOtherTypeUrl(customUrl.url);
// Save custom URL
await testSubjects.click('mlJobAddCustomUrl');
const expectedIndex = existingCustomUrls.length;
await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label);
// Save the job
await this.saveEditJobFlyoutChanges();
}
public async editCustomUrl(
jobId: string,
indexInList: number,
customUrl: { label: string; url: string }
) {
await this.openEditCustomUrlsForJobTab(jobId);
await customUrls.editCustomUrlLabel(indexInList, customUrl.label);
await customUrls.editCustomUrlUrlValue(indexInList, customUrl.url);
// Save the edit
await this.saveEditJobFlyoutChanges();
}
public async deleteCustomUrl(jobId: string, indexInList: number) {
await this.openEditCustomUrlsForJobTab(jobId);
const beforeCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel');
await customUrls.deleteCustomUrl(indexInList);
// Save the edit and check the custom URL has been deleted.
await testSubjects.click('mlEditJobFlyoutSaveButton');
await this.openEditCustomUrlsForJobTab(jobId);
await customUrls.assertCustomUrlsLength(beforeCustomUrls.length - 1);
await this.closeEditJobFlyout();
}
public async openTestCustomUrl(jobId: string, indexInList: number) {
await this.openEditCustomUrlsForJobTab(jobId);
await customUrls.clickTestCustomUrl(indexInList);
}
public async testDiscoverCustomUrlAction(expectedHitCountFormatted: string) {
await customUrls.assertDiscoverCustomUrlAction(expectedHitCountFormatted);
}
public async testDashboardCustomUrlAction(expectedPanelCount: number) {
await customUrls.assertDashboardCustomUrlAction(expectedPanelCount);
}
public async testOtherTypeCustomUrlAction(
jobId: string,
indexInList: number,
expectedUrl: string
) {
// Can't test the contents of the external page, so just check the expected URL.
await this.openEditCustomUrlsForJobTab(jobId);
await customUrls.assertCustomUrlUrlValue(indexInList, expectedUrl);
await this.closeEditJobFlyout();
}
})();
}

View file

@ -527,7 +527,7 @@ export function MachineLearningJobWizardCommonProvider(
const expectedIndex = existingCustomUrls.length;
await customUrls.assertCustomUrlItem(expectedIndex, customUrl.label);
await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label);
},
async ensureAdvancedSectionOpen() {

View file

@ -305,6 +305,10 @@ export function MachineLearningTestResourcesProvider({ getService }: FtrProvider
await this.createDashboardIfNeeded(dashboards.mlTestDashboard);
},
async deleteMLTestDashboard() {
await this.deleteDashboardByTitle(dashboards.mlTestDashboard.requestBody.attributes.title);
},
async createDashboardIfNeeded(dashboard: any) {
const title = dashboard.requestBody.attributes.title;
const dashboardId = await this.getDashboardId(title);