Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .buildkite/ftr-manifests/ftr_platform_stateful_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ enabled:
- src/platform/test/functional/apps/management/group2/config.ts
- src/platform/test/functional/apps/management/group3/config.ts
- src/platform/test/functional/apps/management/group4/config.ts
- src/platform/test/functional/apps/saved_objects_management/config.ts
- src/platform/test/functional/apps/status_page/config.ts
- src/platform/test/functional/apps/visualize/group1/config.ts
- src/platform/test/functional/apps/visualize/group2/config.ts
Expand Down Expand Up @@ -319,7 +318,6 @@ enabled:
- x-pack/platform/test/functional/apps/remote_clusters/config.ts
- x-pack/platform/test/functional/apps/reporting_management/config.ts
- x-pack/platform/test/functional/apps/rollup_job/config.ts
- x-pack/platform/test/functional/apps/saved_objects_management/config.ts
- x-pack/platform/test/functional/apps/security/config.ts
- x-pack/platform/test/functional/apps/snapshot_restore/config.ts
- x-pack/platform/test/functional/apps/spaces/config.ts
Expand Down
1 change: 1 addition & 0 deletions .buildkite/scout_ci_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ plugins:
- logstash
- maps
- ml
- monitoring
- navigation
- observability
- observability_onboarding
Expand Down
4 changes: 0 additions & 4 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -2248,9 +2248,7 @@ x-pack/platform/plugins/shared/ml/server/models/data_recognizer/modules/security
# Core
/src/platform/test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json @elastic/kibana-core @elastic/kibana-data-discovery
/src/platform/test/functional/fixtures/kbn_archiver/saved_search.json @elastic/kibana-core # Assigned per only use: https://github.com/elastic/kibana/blob/main/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts#L100
/src/platform/test/functional/fixtures/kbn_archiver/saved_objects_management/show_relationships.json @elastic/kibana-core # Assigned per only use: https://github.com/elastic/kibana/blob/main/test/functional/apps/saved_objects_management/show_relationships.ts#L20
/src/platform/test/functional/fixtures/kbn_archiver/saved_objects_management/hidden_from_http_apis.json @elastic/kibana-core
/src/platform/test/functional/fixtures/kbn_archiver/saved_objects_management/edit_saved_object.json @elastic/kibana-core # Assigned per only use: https://github.com/elastic/kibana/blob/main/test/functional/apps/saved_objects_management/inspect_saved_objects.ts#L40
/src/platform/test/functional/fixtures/es_archiver/saved_objects_management @elastic/kibana-core
/src/platform/test/api_integration/fixtures/es_archiver/saved_objects @elastic/kibana-core
/src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects @elastic/kibana-core
Expand Down Expand Up @@ -2321,7 +2319,6 @@ x-pack/platform/test/plugin_api_integration/test_suites/platform/ @elastic/kiban
/src/platform/test/api_integration/apis/stats @elastic/kibana-core # Assigned per: https://github.com/elastic/kibana/pull/20577
/src/platform/test/api_integration/apis/saved_objects* @elastic/kibana-core
/src/platform/test/api_integration/apis/core/*.ts @elastic/kibana-core
/x-pack/platform/test/functional/apps/saved_objects_management @elastic/kibana-core
/x-pack/platform/test/usage_collection @elastic/kibana-core
/x-pack/platform/test/licensing_plugin @elastic/kibana-core
/x-pack/platform/test/functional_execution_context @elastic/kibana-core
Expand Down Expand Up @@ -2666,7 +2663,6 @@ x-pack/platform/test/functional/page_objects/search_profiler_page.ts @elastic/se
/src/platform/test/functional/page_objects/embedded_console.ts @elastic/kibana-management
/src/platform/test/functional/page_objects/console_page.ts @elastic/kibana-management
/src/platform/test/functional/firefox/console.config.ts @elastic/kibana-management
/src/platform/test/functional/apps/saved_objects_management @elastic/kibana-management
/src/platform/test/functional/apps/console/*.ts @elastic/kibana-management
/src/platform/test/api_integration/apis/console/*.ts @elastic/kibana-management
/src/platform/test/accessibility/apps/management.ts @elastic/kibana-management
Expand Down
2 changes: 2 additions & 0 deletions src/platform/packages/shared/kbn-scout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ export * from './src/playwright/ui_components';
// Page-object wrappers and helpers for shared Kibana surfaces.
export {
ContentListWrapper,
CopySavedObjectsToSpaceFlyout,
buildContentListSearch,
buildContentListUrlRegex,
DataViewsManagementPage,
ListingTable,
SavedObjectsManagementPage,
} from './src/playwright/page_objects';
export type { ContentListUrlState } from './src/playwright/page_objects';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { Locator } from 'playwright/test';
import type { ScoutPage } from '..';

export interface CopyToSpaceSetupOptions {
destinationSpaceId: string;
createNewCopies?: boolean;
overwrite?: boolean;
}

export interface CopyToSpaceSummary {
success: number;
pending: number;
skipped: number;
errors: number;
}

// EuiStat renders as "<label>\n<count>" — take the trailing number.
const parseStat = (text: string): number => {
const parts = text.trim().split('\n');
const last = parts[parts.length - 1];
const parsed = Number.parseInt(last, 10);
if (Number.isNaN(parsed)) {
throw new Error(`Could not parse a number from stat text '${text}'`);
}
return parsed;
};

export class CopySavedObjectsToSpaceFlyout {
public readonly flyout: Locator;
public readonly form: Locator;
public readonly initiateButton: Locator;
public readonly finishButton: Locator;
public readonly summarySuccessCount: Locator;
public readonly summaryPendingCount: Locator;
public readonly summarySkippedCount: Locator;
public readonly summaryErrorCount: Locator;

constructor(private readonly page: ScoutPage) {
this.flyout = this.page.testSubj.locator('copy-to-space-flyout');
this.form = this.page.testSubj.locator('copy-to-space-form');
this.initiateButton = this.page.testSubj.locator('cts-initiate-button');
this.finishButton = this.page.testSubj.locator('cts-finish-button');
this.summarySuccessCount = this.page.testSubj.locator('cts-summary-success-count');
this.summaryPendingCount = this.page.testSubj.locator('cts-summary-pending-count');
this.summarySkippedCount = this.page.testSubj.locator('cts-summary-skipped-count');
this.summaryErrorCount = this.page.testSubj.locator('cts-summary-error-count');
}

/** Asserts the flyout is open. */
async waitForOpen(): Promise<void> {
await this.flyout.waitFor({ state: 'visible' });
}

/**
* Configures the flyout form prior to initiating the copy. When
* `createNewCopies` is `false`, toggles the matching radio; if `overwrite`
* is also `false`, picks the "do not overwrite" radio. Selects the
* destination space via `cts-space-selector-row-${spaceId}`.
*/
async setupForm({
createNewCopies = true,
overwrite = false,
destinationSpaceId,
}: CopyToSpaceSetupOptions): Promise<void> {
if (createNewCopies && overwrite) {
throw new Error('createNewCopies and overwrite options cannot be used together');
}

if (!createNewCopies) {
await this.form.locator('label[for="createNewCopiesDisabled"]').click();
if (!overwrite) {
await this.page.testSubj
.locator('cts-copyModeControl-overwriteRadioGroup')
.locator('label[for="overwriteDisabled"]')
.click();
}
}

await this.page.testSubj.locator(`cts-space-selector-row-${destinationSpaceId}`).click();
}

async startCopy(): Promise<void> {
await this.initiateButton.click();
}

/**
* Waits for the per-space loading indicator to disappear and the success
* indicator to appear for the supplied destination space.
*/
async waitForCopyToFinish(destinationSpaceId: string): Promise<void> {
const loading = this.page.testSubj.locator(
`cts-summary-indicator-loading-${destinationSpaceId}`
);
// Fast copies may skip the loading indicator entirely; swallow the wait.
await loading.waitFor({ state: 'detached', timeout: 30_000 }).catch(() => {});
await this.page.testSubj
.locator(`cts-summary-indicator-success-${destinationSpaceId}`)
.waitFor({ state: 'visible', timeout: 30_000 });
}

/** Reads the four EuiStat counters in the flyout summary. */
async getSummaryCounts(): Promise<CopyToSpaceSummary> {
return {
success: parseStat(await this.summarySuccessCount.innerText()),
pending: parseStat(await this.summaryPendingCount.innerText()),
skipped: parseStat(await this.summarySkippedCount.innerText()),
errors: parseStat(await this.summaryErrorCount.innerText()),
};
}

/** Closes the flyout and waits for it to disappear. */
async finishCopy(): Promise<void> {
await this.finishButton.click();
await this.flyout.waitFor({ state: 'detached' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { ScoutPage } from '..';
import type { ScoutLogger } from '../../common';
import type { ScoutTestConfig } from '../../types';
import { CollapsibleNav } from './collapsible_nav';
import { CopySavedObjectsToSpaceFlyout } from './copy_saved_objects_to_space_flyout';
import { DashboardApp } from './dashboard_app';
import { DataViewsManagementPage } from './data_views_management_page';
import { DashboardLinks } from './dashboard_links';
Expand All @@ -20,6 +21,7 @@ import { FilterBar } from './filter_bar';
import { MapsPage } from './maps_page';
import { QueryBar } from './query_bar';
import { RenderablePage } from './renderable_page';
import { SavedObjectsManagementPage } from './saved_objects_management_page';
import { SavedQueryManagementMenu } from './saved_query_management_menu';
import { Toasts } from './toasts';
import { createLazyPageObject } from './utils';
Expand All @@ -40,8 +42,10 @@ import type { KibanaUrl } from '../../common/services/kibana_url';

export {
ContentListWrapper,
CopySavedObjectsToSpaceFlyout,
DataViewsManagementPage,
ListingTable,
SavedObjectsManagementPage,
buildContentListSearch,
buildContentListUrlRegex,
};
Expand All @@ -66,6 +70,8 @@ export interface PageObjects {
maps: MapsPage;
queryBar: QueryBar;
renderable: RenderablePage;
savedObjectsManagement: SavedObjectsManagementPage;
copySavedObjectsToSpaceFlyout: CopySavedObjectsToSpaceFlyout;
savedQueryManagementMenu: SavedQueryManagementMenu;
collapsibleNav: CollapsibleNav;
toasts: Toasts;
Expand Down Expand Up @@ -95,6 +101,15 @@ export function createCorePageObjects(fixtures: PageObjectsFixtures): PageObject
maps: createLazyPageObject(MapsPage, fixtures.page),
queryBar: createLazyPageObject(QueryBar, fixtures.page),
renderable: createLazyPageObject(RenderablePage, fixtures.page),
savedObjectsManagement: createLazyPageObject(
SavedObjectsManagementPage,
fixtures.page,
fixtures.kbnUrl
),
copySavedObjectsToSpaceFlyout: createLazyPageObject(
CopySavedObjectsToSpaceFlyout,
fixtures.page
),
savedQueryManagementMenu: createLazyPageObject(SavedQueryManagementMenu, fixtures.page),
collapsibleNav: createLazyPageObject(CollapsibleNav, fixtures.page, fixtures.config),
toasts: createLazyPageObject(Toasts, fixtures.page),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { Locator } from 'playwright/test';
import { expect, type ScoutPage } from '..';
import type { KibanaUrl } from '../../common/services/kibana_url';
import { KibanaCodeEditorWrapper } from '../ui_components';

const spacePrefix = (spaceId?: string) => (spaceId && spaceId !== 'default' ? `/s/${spaceId}` : '');

/** Page object for the Saved Objects Management UI. */
export class SavedObjectsManagementPage {
public readonly table: Locator;
public readonly selectAllCheckbox: Locator;
public readonly deleteListButton: Locator;
public readonly inspectDeleteButton: Locator;
public readonly inspectSaveButton: Locator;
public readonly codeEditor: Locator;
public readonly appNotFoundPageContent: Locator;
public readonly searchBar: Locator;
public readonly importTrigger: Locator;
public readonly importSubmit: Locator;
public readonly importSuccess: Locator;
public readonly importDone: Locator;
public readonly codeEditorWrapper: KibanaCodeEditorWrapper;

constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) {
this.table = this.page.testSubj.locator('savedObjectsTable');
this.selectAllCheckbox = this.page.testSubj.locator('checkboxSelectAll');
this.deleteListButton = this.page.testSubj.locator('savedObjectsManagementDelete');
this.inspectDeleteButton = this.page.testSubj.locator('savedObjectEditDelete');
this.inspectSaveButton = this.page.testSubj.locator('savedObjectEditSave');
this.codeEditor = this.page.testSubj.locator('kibanaCodeEditor');
this.appNotFoundPageContent = this.page.testSubj.locator('appNotFoundPageContent');
this.searchBar = this.page.testSubj.locator('savedObjectSearchBar');
this.importTrigger = this.page.testSubj.locator('importObjects');
this.importSubmit = this.page.testSubj.locator('importSavedObjectsImportBtn');
this.importSuccess = this.page.testSubj.locator('importSavedObjectsSuccess');
this.importDone = this.page.testSubj.locator('importSavedObjectsDoneBtn');
this.codeEditorWrapper = new KibanaCodeEditorWrapper(this.page);
}

/**
* Navigates to the SOM listing page. Does NOT wait for the table so the
* caller can also use it for negative tests (app-not-found / 404).
*/
async gotoListing(spaceId?: string): Promise<void> {
await this.page.goto(this.kbnUrl.get(`${spacePrefix(spaceId)}/app/management/kibana/objects`));
}

/** Waits for the saved-objects table to render at least one row. */
async waitForTableLoaded(): Promise<void> {
await this.table.waitFor({ state: 'visible' });
await expect(this.page.testSubj.locator('savedObjectsTableRowTitle')).not.toHaveCount(0);
}

/** Navigates directly to the SOM inspect view for a specific saved object. */
async gotoInspect(type: string, id: string, spaceId?: string): Promise<void> {
await this.page.goto(
this.kbnUrl.get(`${spacePrefix(spaceId)}/app/management/kibana/objects/${type}/${id}`)
);
}

/** Reads the full Monaco editor model (bypasses viewport virtualisation). */
async getCodeEditorValue(): Promise<string> {
await this.codeEditor.waitFor({ state: 'visible' });
return this.codeEditorWrapper.getCodeEditorValue();
}

/** Visible row titles in the table, stripped of EuiLink trailing glyphs. */
async getRowTitles(): Promise<string[]> {
await this.waitForTableLoaded();
const texts = await this.page.testSubj.locator('savedObjectsTableRowTitle').allInnerTexts();
return texts.map((text) => text.split('\n')[0].trim());
}

/** Types into the search bar and waits for the table to refilter. */
async searchFor(query: string): Promise<void> {
await this.searchBar.fill('');
await this.searchBar.fill(query);
await this.searchBar.press('Enter');
await this.waitForTableLoaded();
}

/** Imports an .ndjson file via the SOM "Import" flow with overwrite enabled. */
async importFile(absoluteFilePath: string): Promise<void> {
await this.importTrigger.click();
// EuiFilePicker has no stable test-subj; drive its underlying input directly.
await this.page.locator('input[type="file"][accept=".ndjson"]').setInputFiles(absoluteFilePath);
await this.importSubmit.click();
await this.importSuccess.waitFor({ state: 'visible', timeout: 30_000 });
await this.importDone.click();
await this.table.waitFor({ state: 'visible' });
}

/** Opens the row context menu for the given title and clicks "Inspect". */
async clickInspectByTitle(title: string): Promise<void> {
const menu = await this.openRowContextMenu(title);
await menu.locator('[data-test-subj="savedObjectsTableAction-inspect"]').click();
}

/** Opens the row context menu for the given title and clicks "Copy to space". */
async clickCopyToSpaceByTitle(title: string): Promise<void> {
const menu = await this.openRowContextMenu(title);
await menu
.locator('[data-test-subj="savedObjectsTableAction-copy_saved_objects_to_space"]')
.click();
}

/** Clicks the inspect-view delete button and confirms the modal. */
async deleteFromInspect(): Promise<void> {
await this.inspectDeleteButton.waitFor({ state: 'visible' });
await this.inspectDeleteButton.click();
const confirmTitle = this.page.testSubj.locator('confirmModalTitleText');
await confirmTitle.waitFor({ state: 'visible' });
await this.page.testSubj.locator('confirmModalConfirmButton').click();
await confirmTitle.waitFor({ state: 'hidden' });
}

private async openRowContextMenu(title: string): Promise<Locator> {
// `filter({ hasText })` keeps titles with punctuation matchable.
const titleLocator = this.page.testSubj
.locator('savedObjectsTableRowTitle')
.filter({ hasText: title });
const row = this.page
.locator('[data-test-subj~="savedObjectsTableRow"]')
.filter({ has: titleLocator });
await row.waitFor({ state: 'visible' });
await row.locator('[data-test-subj="euiCollapsedItemActionsButton"]').click();
const menuPanel = this.page.locator('.euiContextMenuPanel');
await menuPanel.waitFor({ state: 'visible' });
return menuPanel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependsOn:
- '@kbn/css-utils'
- '@kbn/content-management-plugin'
- '@kbn/scout'
- '@kbn/repo-info'
tags:
- plugin
- prod
Expand Down
Loading
Loading