Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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