kibana/test/functional/page_objects/dashboard_page.ts
Eyo O. Eyo 690690ea21
Simplify workflow for dashboard copy creation in both view and edit interaction modes (#180938)
## Summary

Closes https://github.com/elastic/kibana/issues/161047

- Removes the `save as` top nav menu button
- Also renames nav menu item `clone` to `duplicate` and make it
available in edit mode.
- The save dashboard modal no longer displays and open to save the
dashboard in context as new, given that we've chosen to explicitly
create a copy of the dashboard in context when either of the the
`duplicate` or `saveas` menu option is selected.
- includes bug fix for an issue where clicking the dashboard modal
scrolled the user to the content bottom, see
https://github.com/elastic/kibana/pull/180938#issuecomment-2117586572

## Before
### View mode
<img width="1728" alt="Screenshot 2024-04-16 at 15 59 10"
src="48dc4565-1f75-4f46-839c-8d76f4fedefe">

### Edit mode
<img width="1725" alt="Screenshot 2024-04-16 at 15 59 00"
src="1ac743ac-33b4-4f68-ab59-ad19ab58fa1c">

## After

#### Managed Dashboard

5072a501-8d16-4f25-9575-6f11fed6e580

#### View mode

610d0952-97f0-46b8-a0ea-1546a799d387

#### Edit mode

4f596c07-7bd1-4c5a-9131-0c78731cb113



<!-- ### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces&mdash;unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes&mdash;Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
2024-05-29 11:46:23 +02:00

880 lines
31 KiB
TypeScript

/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const PIE_CHART_VIS_NAME = 'Visualization PieChart';
export const AREA_CHART_VIS_NAME = 'Visualization漢字 AreaChart';
export const LINE_CHART_VIS_NAME = 'Visualization漢字 LineChart';
import expect from '@kbn/expect';
import { FtrService } from '../ftr_provider_context';
import { CommonPageObject } from './common_page';
interface SaveDashboardOptions {
/**
* @default true
*/
waitDialogIsClosed?: boolean;
exitFromEditMode?: boolean;
needsConfirm?: boolean;
storeTimeWithDashboard?: boolean;
saveAsNew?: boolean;
tags?: string[];
}
interface AddNewDashboardOptions {
continueEditing?: boolean;
expectWarning?: boolean;
}
export class DashboardPageObject extends FtrService {
private readonly comboBox = this.ctx.getService('comboBox');
private readonly config = this.ctx.getService('config');
private readonly log = this.ctx.getService('log');
private readonly find = this.ctx.getService('find');
private readonly retry = this.ctx.getService('retry');
private readonly browser = this.ctx.getService('browser');
private readonly globalNav = this.ctx.getService('globalNav');
private readonly kibanaServer = this.ctx.getService('kibanaServer');
private readonly testSubjects = this.ctx.getService('testSubjects');
private readonly dashboardAddPanel = this.ctx.getService('dashboardAddPanel');
private readonly renderable = this.ctx.getService('renderable');
private readonly listingTable = this.ctx.getService('listingTable');
private readonly elasticChart = this.ctx.getService('elasticChart');
private readonly common = this.ctx.getPageObject('common');
private readonly header = this.ctx.getPageObject('header');
private readonly visualize = this.ctx.getPageObject('visualize');
private readonly discover = this.ctx.getPageObject('discover');
private readonly appsMenu = this.ctx.getService('appsMenu');
private readonly toasts = this.ctx.getService('toasts');
private readonly logstashIndex = this.config.get('esTestCluster.ccs')
? 'ftr-remote:logstash-*'
: 'logstash-*';
private readonly kibanaIndex = this.config.get('esTestCluster.ccs')
? 'test/functional/fixtures/kbn_archiver/ccs/dashboard/legacy/legacy.json'
: 'test/functional/fixtures/kbn_archiver/dashboard/legacy/legacy.json';
public readonly APP_ID = 'dashboards';
async initTests({ kibanaIndex = this.kibanaIndex, defaultIndex = this.logstashIndex } = {}) {
this.log.debug('load kibana index with visualizations and log data');
await this.kibanaServer.savedObjects.cleanStandardList();
await this.kibanaServer.importExport.load(kibanaIndex);
await this.kibanaServer.uiSettings.replace({ defaultIndex });
await this.navigateToApp();
}
public async navigateToApp() {
await this.common.navigateToApp(this.APP_ID);
}
public async navigateToAppFromAppsMenu() {
await this.retry.try(async () => {
await this.appsMenu.clickLink('Dashboard', { category: 'kibana' });
await this.header.waitUntilLoadingHasFinished();
const currentUrl = await this.browser.getCurrentUrl();
if (!currentUrl.includes('app/dashboard')) {
throw new Error(`Not in dashboard application after clicking 'Dashboard' in apps menu`);
}
});
}
public async expectAppStateRemovedFromURL() {
this.retry.try(async () => {
const url = await this.browser.getCurrentUrl();
expect(url.indexOf('_a')).to.be(-1);
});
}
public async preserveCrossAppState() {
const url = await this.browser.getCurrentUrl();
await this.browser.get(url, false);
await this.header.waitUntilLoadingHasFinished();
}
public async clickFullScreenMode() {
this.log.debug(`clickFullScreenMode`);
await this.testSubjects.click('dashboardFullScreenMode');
await this.testSubjects.exists('exitFullScreenModeButton');
await this.waitForRenderComplete();
}
public async exitFullScreenMode() {
this.log.debug(`exitFullScreenMode`);
const logoButton = await this.getExitFullScreenLogoButton();
await logoButton.moveMouseTo();
await this.clickExitFullScreenTextButton();
}
public async fullScreenModeMenuItemExists() {
return await this.testSubjects.exists('dashboardFullScreenMode');
}
public async exitFullScreenTextButtonExists() {
return await this.testSubjects.exists('exitFullScreenModeText');
}
public async getExitFullScreenTextButton() {
return await this.testSubjects.find('exitFullScreenModeText');
}
public async exitFullScreenLogoButtonExists() {
return await this.testSubjects.exists('exitFullScreenModeButton');
}
public async getExitFullScreenLogoButton() {
return await this.testSubjects.find('exitFullScreenModeButton');
}
public async clickExitFullScreenLogoButton() {
await this.testSubjects.click('exitFullScreenModeButton');
await this.waitForRenderComplete();
}
public async clickExitFullScreenTextButton() {
await this.testSubjects.click('exitFullScreenModeText');
await this.waitForRenderComplete();
}
public async getDashboardIdFromCurrentUrl() {
const currentUrl = await this.browser.getCurrentUrl();
const id = this.getDashboardIdFromUrl(currentUrl);
this.log.debug(`Dashboard id extracted from ${currentUrl} is ${id}`);
return id;
}
public getDashboardIdFromUrl(url: string) {
const urlSubstring = '#/view/';
const startOfIdIndex = url.indexOf(urlSubstring) + urlSubstring.length;
const endIndex = url.indexOf('?');
const id = url.substring(startOfIdIndex, endIndex < 0 ? url.length : endIndex);
return id;
}
public async expectUnsavedChangesListingExists(title: string) {
this.log.debug(`Expect Unsaved Changes Listing Exists for `, title);
await this.testSubjects.existOrFail(`edit-unsaved-${title.split(' ').join('-')}`);
}
public async expectUnsavedChangesListingDoesNotExist(title: string) {
this.log.debug(`Expect Unsaved Changes Listing Does Not Exist for `, title);
await this.testSubjects.missingOrFail(`edit-unsaved-${title.split(' ').join('-')}`);
}
public async clickUnsavedChangesContinueEditing(title: string) {
this.log.debug(`Click Unsaved Changes Continue Editing `, title);
await this.testSubjects.existOrFail(`edit-unsaved-${title.split(' ').join('-')}`);
await this.testSubjects.click(`edit-unsaved-${title.split(' ').join('-')}`);
}
public async clickUnsavedChangesDiscard(testSubject: string, confirmDiscard = true) {
this.log.debug(`Click Unsaved Changes Discard for `, testSubject);
await this.testSubjects.existOrFail(testSubject);
await this.testSubjects.click(testSubject);
if (confirmDiscard) {
await this.common.clickConfirmOnModal();
} else {
await this.common.clickCancelOnModal();
}
}
/**
* Returns true if already on the dashboard landing page (that page doesn't have a link to itself).
* @returns {Promise<boolean>}
*/
public async onDashboardLandingPage() {
this.log.debug(`onDashboardLandingPage`);
return await this.listingTable.onListingPage('dashboard');
}
public async expectExistsDashboardLandingPage() {
this.log.debug(`expectExistsDashboardLandingPage`);
await this.testSubjects.existOrFail('dashboardLandingPage');
}
public async expectOnDashboard(expectedTitle: string) {
await this.retry.waitFor(
`last breadcrumb to have dashboard title: ${expectedTitle} OR Editing ${expectedTitle}`,
async () => {
const actualTitle = await this.globalNav.getLastBreadcrumb();
this.log.debug(`Expected dashboard title ${expectedTitle}, actual: ${actualTitle}`);
return actualTitle === expectedTitle || actualTitle === `Editing ${expectedTitle}`;
}
);
}
public async gotoDashboardLandingPage(ignorePageLeaveWarning = true) {
this.log.debug('gotoDashboardLandingPage');
if (await this.onDashboardLandingPage()) return;
const breadcrumbLink = this.config.get('serverless')
? 'breadcrumb breadcrumb-deepLinkId-dashboards'
: 'breadcrumb dashboardListingBreadcrumb first';
await this.testSubjects.click(breadcrumbLink);
await this.retry.try(async () => {
const warning = await this.testSubjects.exists('confirmModalTitleText');
if (warning) {
await this.testSubjects.click(
ignorePageLeaveWarning ? 'confirmModalConfirmButton' : 'confirmModalCancelButton'
);
}
});
await this.expectExistsDashboardLandingPage();
}
public async duplicateDashboard(dashboardNameOverride?: string) {
this.log.debug('Clicking duplicate');
await this.testSubjects.click('dashboardInteractiveSaveMenuItem');
if (dashboardNameOverride) {
this.log.debug('entering dashboard duplicate override title');
await this.testSubjects.setValue('savedObjectTitle', dashboardNameOverride);
}
await this.clickSave();
// Confirm that the Dashboard has actually been saved
await this.testSubjects.existOrFail('saveDashboardSuccess');
}
/**
* Asserts that the duplicate title warning is either displayed or not displayed.
* @param { displayed: boolean }
*/
public async expectDuplicateTitleWarningDisplayed({ displayed = true }) {
if (displayed) {
await this.testSubjects.existOrFail('titleDuplicateWarnMsg');
} else {
await this.testSubjects.missingOrFail('titleDuplicateWarnMsg');
}
}
/**
* Asserts that the toolbar pagination (count and arrows) is either displayed or not displayed.
*/
public async expectToolbarPaginationDisplayed() {
const isLegacyDefault = await this.discover.useLegacyTable();
if (isLegacyDefault) {
const subjects = [
'pagination-button-previous',
'pagination-button-next',
'toolBarTotalDocsText',
];
await Promise.all(subjects.map(async (subj) => await this.testSubjects.existOrFail(subj)));
} else {
const subjects = ['pagination-button-previous', 'pagination-button-next'];
await Promise.all(subjects.map(async (subj) => await this.testSubjects.existOrFail(subj)));
const paginationListExists = await this.find.existsByCssSelector('.euiPagination__list');
if (!paginationListExists) {
throw new Error(`expected discover data grid pagination list to exist`);
}
}
}
public async switchToEditMode() {
this.log.debug('Switching to edit mode');
if (await this.testSubjects.exists('dashboardEditMode')) {
// if the dashboard is not already in edit mode
await this.testSubjects.click('dashboardEditMode');
}
// wait until the count of dashboard panels equals the count of toggle menu icons
await this.retry.waitFor('in edit mode', async () => {
const panels = await this.testSubjects.findAll('embeddablePanel', 2500);
const menuIcons = await this.testSubjects.findAll('embeddablePanelToggleMenuIcon', 2500);
return panels.length === menuIcons.length;
});
}
public async getIsInViewMode() {
this.log.debug('getIsInViewMode');
return await this.testSubjects.exists('dashboardEditMode');
}
public async ensureDashboardIsInEditMode() {
if (await this.getIsInViewMode()) {
await this.switchToEditMode();
}
await this.waitForRenderComplete();
}
public async clickCancelOutOfEditMode(accept = true) {
this.log.debug('clickCancelOutOfEditMode');
if (await this.getIsInViewMode()) return;
await this.retry.waitFor('leave edit mode button enabled', async () => {
const leaveEditModeButton = await this.testSubjects.find('dashboardViewOnlyMode');
const isDisabled = await leaveEditModeButton.getAttribute('disabled');
return !isDisabled;
});
await this.testSubjects.click('dashboardViewOnlyMode');
if (accept) {
const confirmation = await this.testSubjects.exists('confirmModalTitleText');
if (confirmation) {
await this.common.clickConfirmOnModal();
}
}
}
public async clickDiscardChanges(accept = true) {
await this.retry.try(async () => {
await this.expectDiscardChangesButtonEnabled();
this.log.debug('clickDiscardChanges');
await this.testSubjects.click('dashboardDiscardChangesMenuItem');
});
await this.common.expectConfirmModalOpenState(true);
if (accept) {
await this.common.clickConfirmOnModal();
}
}
public async clickQuickSave() {
await this.retry.try(async () => {
await this.expectQuickSaveButtonEnabled();
this.log.debug('clickQuickSave');
await this.testSubjects.click('dashboardQuickSaveMenuItem');
});
}
public async clearUnsavedChanges() {
this.log.debug('clearUnsavedChanges');
let switchMode = false;
if (await this.getIsInViewMode()) {
await this.switchToEditMode();
switchMode = true;
}
await this.retry.try(async () => {
// avoid flaky test by surrounding in retry
await this.testSubjects.existOrFail('dashboardUnsavedChangesBadge');
await this.clickQuickSave();
await this.testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
await this.testSubjects.click('toastCloseButton');
});
if (switchMode) {
await this.clickCancelOutOfEditMode();
}
}
public async expectUnsavedChangesBadge() {
this.log.debug('Expect unsaved changes badge to be present');
await this.retry.try(async () => {
await this.testSubjects.existOrFail('dashboardUnsavedChangesBadge');
});
}
public async expectMissingUnsavedChangesBadge() {
this.log.debug('Expect there to be no unsaved changes badge');
await this.retry.try(async () => {
await this.testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
});
}
public async clickNewDashboard(
options: AddNewDashboardOptions = { continueEditing: false, expectWarning: false }
) {
const { continueEditing, expectWarning } = options;
const discardButtonExists = await this.testSubjects.exists('discardDashboardPromptButton');
if (!continueEditing && discardButtonExists) {
this.log.debug('found discard button');
await this.testSubjects.click('discardDashboardPromptButton');
const confirmation = await this.testSubjects.exists('confirmModalTitleText');
if (confirmation) {
await this.common.clickConfirmOnModal();
}
}
await this.listingTable.clickNewButton();
if (expectWarning) {
await this.testSubjects.existOrFail('dashboardCreateConfirm');
}
if (await this.testSubjects.exists('dashboardCreateConfirm')) {
if (continueEditing) {
await this.testSubjects.click('dashboardCreateConfirmContinue');
} else {
await this.testSubjects.click('dashboardCreateConfirmStartOver');
}
}
// make sure the dashboard page is shown
await this.waitForRenderComplete();
}
public async clickCreateDashboardPrompt() {
await this.testSubjects.click('newItemButton');
}
public async getCreateDashboardPromptExists() {
return this.testSubjects.exists('emptyListPrompt');
}
public async isSettingsOpen() {
this.log.debug('isSettingsOpen');
return await this.testSubjects.exists('dashboardSettingsMenu');
}
public async openSettingsFlyout() {
this.log.debug('openSettingsFlyout');
const isOpen = await this.isSettingsOpen();
if (!isOpen) {
return await this.testSubjects.click('dashboardSettingsButton');
}
}
// avoids any 'Object with id x not found' errors when switching tests.
public async clearSavedObjectsFromAppLinks() {
await this.header.clickVisualize();
await this.visualize.gotoLandingPage();
await this.navigateToAppFromAppsMenu();
await this.gotoDashboardLandingPage();
}
public async gotoDashboardEditMode(dashboardName: string) {
await this.loadSavedDashboard(dashboardName);
await this.switchToEditMode();
}
public async gotoDashboardURL({
id,
args,
editMode,
}: {
id?: string;
editMode?: boolean;
args?: Parameters<InstanceType<typeof CommonPageObject>['navigateToActualUrl']>[2];
} = {}) {
let dashboardLocation = `/create`;
if (id) {
const edit = editMode ? `?_a=(viewMode:edit)` : '';
dashboardLocation = `/view/${id}${edit}`;
}
this.common.navigateToActualUrl('dashboard', dashboardLocation, args);
}
public async gotoDashboardListingURL({
args,
}: {
args?: Parameters<InstanceType<typeof CommonPageObject>['navigateToActualUrl']>[2];
} = {}) {
await this.common.navigateToActualUrl('dashboard', '/list', args);
}
public async renameDashboard(dashboardName: string) {
this.log.debug(`Naming dashboard ` + dashboardName);
await this.testSubjects.click('dashboardRenameButton');
await this.testSubjects.setValue('savedObjectTitle', dashboardName);
}
/**
* @description opens the dashboard settings flyout to modify an existing dashboard
*/
public async modifyExistingDashboardDetails(
dashboard: string,
saveOptions: Pick<SaveDashboardOptions, 'storeTimeWithDashboard' | 'tags' | 'needsConfirm'> = {}
) {
await this.openSettingsFlyout();
await this.retry.try(async () => {
this.log.debug('entering new title');
await this.testSubjects.setValue('dashboardTitleInput', dashboard);
if (saveOptions.storeTimeWithDashboard !== undefined) {
await this.setStoreTimeWithDashboard(saveOptions.storeTimeWithDashboard);
}
if (saveOptions.tags) {
const tagsComboBox = await this.testSubjects.find('comboBoxInput');
for (const tagName of saveOptions.tags) {
await this.comboBox.setElement(tagsComboBox, tagName);
}
}
this.log.debug('DashboardPage.applyCustomization');
await this.testSubjects.click('applyCustomizeDashboardButton');
if (saveOptions.needsConfirm) {
await this.ensureDuplicateTitleCallout();
await this.testSubjects.click('applyCustomizeDashboardButton');
}
this.log.debug('isCustomizeDashboardLoadingIndicatorVisible');
return await this.testSubjects.exists('dashboardUnsavedChangesBadge', { timeout: 1500 });
});
}
/**
* @description Save the current dashboard with the specified name and options and
* verify that the save was successful, close the toast and return the
* toast message
*/
public async saveDashboard(
dashboardName: string,
saveOptions: SaveDashboardOptions = {
waitDialogIsClosed: true,
exitFromEditMode: true,
saveAsNew: true,
}
) {
await this.retry.try(async () => {
if (saveOptions.saveAsNew) {
await this.enterDashboardSaveModalApplyUpdatesAndClickSave(dashboardName, saveOptions);
} else {
await this.modifyExistingDashboardDetails(dashboardName, saveOptions);
await this.clickQuickSave();
}
if (saveOptions.needsConfirm) {
await this.ensureDuplicateTitleCallout();
await this.clickSave();
}
// Confirm that the Dashboard has actually been saved
await this.testSubjects.existOrFail('saveDashboardSuccess');
});
let message;
if (saveOptions.saveAsNew) {
message = await this.toasts.getTitleAndDismiss();
await this.header.waitUntilLoadingHasFinished();
await this.common.waitForSaveModalToClose();
}
const isInViewMode = await this.testSubjects.exists('dashboardEditMode');
if (saveOptions.exitFromEditMode && !isInViewMode) {
await this.clickCancelOutOfEditMode();
}
await this.header.waitUntilLoadingHasFinished();
return message;
}
public async cancelSave() {
this.log.debug('Canceling save');
await this.testSubjects.click('saveCancelButton');
}
public async clickSave() {
this.log.debug('DashboardPage.clickSave');
await this.testSubjects.click('confirmSaveSavedObjectButton');
}
/**
* @description populates the duplicate dashboard modal
*/
public async enterDashboardSaveModalApplyUpdatesAndClickSave(
dashboardTitle: string,
saveOptions: Omit<SaveDashboardOptions, 'saveAsNew'> = { waitDialogIsClosed: true }
) {
const isSaveModalOpen = await this.testSubjects.exists('savedObjectSaveModal', {
timeout: 2000,
});
if (!isSaveModalOpen) {
await this.testSubjects.click('dashboardInteractiveSaveMenuItem');
}
const modalDialog = await this.testSubjects.find('savedObjectSaveModal');
this.log.debug('entering new title');
await this.testSubjects.setValue('savedObjectTitle', dashboardTitle);
if (saveOptions.storeTimeWithDashboard !== undefined) {
await this.setStoreTimeWithDashboard(saveOptions.storeTimeWithDashboard);
}
if (saveOptions.tags) {
await this.selectDashboardTags(saveOptions.tags);
}
await this.clickSave();
if (saveOptions.waitDialogIsClosed) {
await this.testSubjects.waitForDeleted(modalDialog);
}
}
public async ensureDuplicateTitleCallout() {
await this.testSubjects.existOrFail('titleDuplicateWarnMsg');
}
public async selectDashboardTags(tagNames: string[]) {
const tagsComboBox = await this.testSubjects.find('savedObjectTagSelector');
for (const tagName of tagNames) {
await this.comboBox.setElement(tagsComboBox, tagName);
}
await this.testSubjects.click('savedObjectTitle');
}
/**
* @param dashboardTitle {String}
*/
public async enterDashboardTitleAndPressEnter(dashboardTitle: string) {
await this.testSubjects.click('dashboardInteractiveSaveMenuItem');
const modalDialog = await this.testSubjects.find('savedObjectSaveModal');
this.log.debug('entering new title');
await this.testSubjects.setValue('savedObjectTitle', dashboardTitle);
await this.common.pressEnterKey();
await this.testSubjects.waitForDeleted(modalDialog);
}
// use the search filter box to narrow the results down to a single
// entry, or at least to a single page of results
public async loadSavedDashboard(dashboardName: string) {
this.log.debug(`Load Saved Dashboard ${dashboardName}`);
await this.gotoDashboardLandingPage();
await this.listingTable.searchForItemWithName(dashboardName, { escape: false });
await this.retry.try(async () => {
await this.listingTable.clickItemLink('dashboard', dashboardName);
await this.header.waitUntilLoadingHasFinished();
// check Dashboard landing page is not present
await this.testSubjects.missingOrFail('dashboardLandingPage', { timeout: 10000 });
});
}
public async getPanelTitles() {
this.log.debug('in getPanelTitles');
const titleObjects = await this.find.allByCssSelector(
'[data-test-subj="embeddablePanelTitleInner"] .embPanel__titleText'
);
return await Promise.all(titleObjects.map(async (title) => await title.getVisibleText()));
}
/**
* @return An array of boolean values - true if the panel title is visible in view mode, false if it is not
*/
public async getVisibilityOfPanelTitles() {
this.log.debug('in getVisibilityOfPanels');
// only works if the dashboard is in view mode
const inViewMode = await this.getIsInViewMode();
if (!inViewMode) {
await this.clickCancelOutOfEditMode();
}
const visibilities: boolean[] = [];
const panels = await this.getDashboardPanels();
for (const panel of panels) {
const exists = await this.find.descendantExistsByCssSelector(
'figcaption.embPanel__header',
panel
);
visibilities.push(exists);
}
// return to edit mode if a switch to view mode above was necessary
if (!inViewMode) {
await this.switchToEditMode();
}
return visibilities;
}
public async getPanels() {
return await this.find.allByCssSelector('.react-grid-item'); // These are gridster-defined elements and classes
}
public async getPanelDimensions() {
const panels = await this.getPanels();
return await Promise.all(
panels.map(async (panel) => {
const size = await panel.getSize();
return {
width: size.width,
height: size.height,
};
})
);
}
public async getPanelCount() {
this.log.debug('getPanelCount');
const panels = await this.testSubjects.findAll('embeddablePanel');
return panels.length;
}
public getTestVisualizations() {
return [
{ name: PIE_CHART_VIS_NAME, description: 'PieChart' },
{ name: 'Visualization☺ VerticalBarChart', description: 'VerticalBarChart' },
{ name: AREA_CHART_VIS_NAME, description: 'AreaChart' },
{ name: 'Visualization☺漢字 DataTable', description: 'DataTable' },
{ name: LINE_CHART_VIS_NAME, description: 'LineChart' },
{ name: 'Visualization MetricChart', description: 'MetricChart' },
];
}
public getTestVisualizationNames() {
return this.getTestVisualizations().map((visualization) => visualization.name);
}
public getTestVisualizationDescriptions() {
return this.getTestVisualizations().map((visualization) => visualization.description);
}
public async getDashboardPanels() {
return await this.testSubjects.findAll('embeddablePanel');
}
public async addVisualizations(visualizations: string[]) {
await this.dashboardAddPanel.addVisualizations(visualizations);
await this.waitForRenderComplete();
}
public async setSaveAsNewCheckBox(checked: boolean) {
this.log.debug('saveAsNewCheckbox: ' + checked);
let saveAsNewCheckbox = await this.testSubjects.find('saveAsNewCheckbox');
const isAlreadyChecked = (await saveAsNewCheckbox.getAttribute('aria-checked')) === 'true';
if (isAlreadyChecked !== checked) {
this.log.debug('Flipping save as new checkbox');
saveAsNewCheckbox = await this.testSubjects.find('saveAsNewCheckbox');
await this.retry.try(() => saveAsNewCheckbox.click());
}
}
public async setStoreTimeWithDashboard(checked: boolean) {
this.log.debug('Storing time with dashboard: ' + checked);
let storeTimeCheckbox = await this.testSubjects.find('storeTimeWithDashboard');
const isAlreadyChecked = (await storeTimeCheckbox.getAttribute('aria-checked')) === 'true';
if (isAlreadyChecked !== checked) {
this.log.debug('Flipping store time checkbox');
storeTimeCheckbox = await this.testSubjects.find('storeTimeWithDashboard');
await this.retry.try(() => storeTimeCheckbox.click());
}
}
public async getSharedItemsCount() {
this.log.debug('in getSharedItemsCount');
const attributeName = 'data-shared-items-count';
const element = await this.find.byCssSelector(`[${attributeName}]`);
if (element) {
const attribute = await element.getAttribute(attributeName);
if (!attribute) throw new Error(`no attribute found for [${attributeName}]`);
return attribute;
}
throw new Error('no element');
}
public async waitForRenderComplete() {
this.log.debug('waitForRenderComplete');
const count = await this.getSharedItemsCount();
// eslint-disable-next-line radix
await this.renderable.waitForRender(parseInt(count));
}
public async verifyNoRenderErrors() {
const errorEmbeddables = await this.testSubjects.findAll('embeddableStackError');
expect(errorEmbeddables.length).to.be(0);
}
public async getSharedContainerData() {
this.log.debug('getSharedContainerData');
const sharedContainer = await this.find.byCssSelector('[data-shared-items-container]');
return {
title: await sharedContainer.getAttribute('data-title'),
description: await sharedContainer.getAttribute('data-description'),
count: await sharedContainer.getAttribute('data-shared-items-count'),
};
}
public async getPanelSharedItemData() {
this.log.debug('in getPanelSharedItemData');
const sharedItemscontainer = await this.find.byCssSelector('[data-shared-items-count]');
const $ = await sharedItemscontainer.parseDomContent();
return $('[data-shared-item]')
.toArray()
.map((item) => {
return {
title: $(item).attr('data-title'),
description: $(item).attr('data-description'),
};
});
}
public async expectMissingSaveOption() {
await this.testSubjects.missingOrFail('dashboardInteractiveSaveMenuItem');
}
public async expectMissingQuickSaveOption() {
await this.testSubjects.missingOrFail('dashboardQuickSaveMenuItem');
}
public async expectExistsQuickSaveOption() {
await this.testSubjects.existOrFail('dashboardQuickSaveMenuItem');
}
public async expectDiscardChangesButtonEnabled() {
this.log.debug('expectDiscardChangesButtonEnabled');
const quickSaveButton = await this.testSubjects.find('dashboardDiscardChangesMenuItem');
const isDisabled = await quickSaveButton.getAttribute('disabled');
if (isDisabled) {
throw new Error('Discard changes button disabled');
}
}
public async expectQuickSaveButtonEnabled() {
this.log.debug('expectQuickSaveButtonEnabled');
const quickSaveButton = await this.testSubjects.find('dashboardQuickSaveMenuItem');
const isDisabled = await quickSaveButton.getAttribute('disabled');
if (isDisabled) {
throw new Error('Quick save button disabled');
}
}
public async expectQuickSaveButtonDisabled() {
this.log.debug('expectQuickSaveButtonDisabled');
const quickSaveButton = await this.testSubjects.find('dashboardQuickSaveMenuItem');
const isDisabled = await quickSaveButton.getAttribute('disabled');
if (!isDisabled) {
throw new Error('Quick save button not disabled');
}
}
public async getNotLoadedVisualizations(vizList: string[]) {
const checkList = [];
for (const name of vizList) {
const isPresent = await this.testSubjects.exists(
`embeddablePanelHeading-${name.replace(/\s+/g, '')}`,
{ timeout: 10000 }
);
checkList.push({ name, isPresent });
}
return checkList.filter((viz) => viz.isPresent === false).map((viz) => viz.name);
}
public async getPanelDrilldownCount(panelIndex = 0): Promise<number> {
this.log.debug('getPanelDrilldownCount');
const panel = (await this.getDashboardPanels())[panelIndex];
try {
const count = await panel.findByTestSubject(
'embeddablePanelNotification-ACTION_PANEL_NOTIFICATIONS'
);
return Number.parseInt(await count.getVisibleText(), 10);
} catch (e) {
// if not found then this is 0 (we don't show badge with 0)
return 0;
}
}
public async getPanelChartDebugState(panelIndex: number) {
return await this.elasticChart.getChartDebugData(undefined, panelIndex);
}
public async isNotificationExists(panelIndex = 0) {
const panel = (await this.getDashboardPanels())[panelIndex];
try {
const notification = await panel.findByClassName('embPanel__optionsMenuPopover-notification');
return Boolean(notification);
} catch (e) {
// if not found then this is false
return false;
}
}
}