From cc8e833de2380545637ce44a2f775df276d47525 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 09:53:40 -0600 Subject: [PATCH 01/34] Add scout tests for feature settings page --- .buildkite/scout_ci_config.yml | 1 + .../test/scout/ui/fixtures/constants.ts | 17 +++++++++ .../test/scout/ui/fixtures/index.ts | 38 +++++++++++++++++++ .../page_objects/feature_settings_page.ts | 21 ++++++++++ .../scout/ui/fixtures/page_objects/index.ts | 8 ++++ .../test/scout/ui/playwright.config.ts | 12 ++++++ .../scout/ui/tests/feature_settings.spec.ts | 29 ++++++++++++++ 7 files changed, 126 insertions(+) create mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/constants.ts create mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/index.ts create mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts create mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/index.ts create mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/playwright.config.ts create mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts diff --git a/.buildkite/scout_ci_config.yml b/.buildkite/scout_ci_config.yml index 6673cf3d5f3d6..4bbd3826c5ae4 100644 --- a/.buildkite/scout_ci_config.yml +++ b/.buildkite/scout_ci_config.yml @@ -34,6 +34,7 @@ plugins: - profiling - search_getting_started - search_homepage + - search_inference_endpoints - security - security_solution - share diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/constants.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/constants.ts new file mode 100644 index 0000000000000..45a9f1c9c3e6e --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/constants.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DEFAULT_START_TIME = '2020-01-01T00:00:00.000Z'; +export const DEFAULT_END_TIME = '2020-01-02T00:00:00.000Z'; + +export const ES_ARCHIVES = { + SOME_ARCHIVE: 'path/to/es_archive', +}; + +export const KBN_ARCHIVES = { + SOME_ARCHIVE: 'path/to/kbn_archive', +}; diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/index.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/index.ts new file mode 100644 index 0000000000000..6ae694e8537ca --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/index.ts @@ -0,0 +1,38 @@ +/* + * 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 type { PageObjects, ScoutTestFixtures, ScoutWorkerFixtures } from '@kbn/scout'; +import { test as baseTest, createLazyPageObject } from '@kbn/scout'; +import { FeatureSettingsPage } from './page_objects'; + +export interface ExtScoutTestFixtures extends ScoutTestFixtures { + pageObjects: PageObjects & { + featureSettings: FeatureSettingsPage; + }; +} + +export const test = baseTest.extend({ + pageObjects: async ( + { + pageObjects, + page, + }: { + pageObjects: ExtScoutTestFixtures['pageObjects']; + page: ExtScoutTestFixtures['page']; + }, + use: (pageObjects: ExtScoutTestFixtures['pageObjects']) => Promise + ) => { + const extendedPageObjects = { + ...pageObjects, + featureSettings: createLazyPageObject(FeatureSettingsPage, page), + }; + + await use(extendedPageObjects); + }, +}); + +export * as testData from './constants'; diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts new file mode 100644 index 0000000000000..7a6d71490c2fb --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -0,0 +1,21 @@ +/* + * 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 type { ScoutPage, Locator } from '@kbn/scout'; + +export class FeatureSettingsPage { + constructor(private readonly page: ScoutPage) {} + + public async goto() { + await this.page.gotoApp('management/modelManagement/model_settings'); + await this.page.testSubj.waitForSelector('modelSettingsPage'); + } + + public get pageHeader(): Locator { + return this.page.testSubj.locator('modelSettingsPageHeader'); + } +} diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/index.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/index.ts new file mode 100644 index 0000000000000..7d58e4af90825 --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { FeatureSettingsPage } from './feature_settings_page'; diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/playwright.config.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/playwright.config.ts new file mode 100644 index 0000000000000..75a7694d12043 --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/playwright.config.ts @@ -0,0 +1,12 @@ +/* + * 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 { createPlaywrightConfig } from '@kbn/scout'; + +export default createPlaywrightConfig({ + testDir: './tests', +}); diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts new file mode 100644 index 0000000000000..18943d5d5f3e3 --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts @@ -0,0 +1,29 @@ +/* + * 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 { tags } from '@kbn/scout'; +import { expect } from '@kbn/scout/ui'; +import { test } from '../fixtures'; + +test.describe('Feature Settings', { tag: tags.deploymentAgnostic }, () => { + test.beforeAll(async ({ uiSettings }) => { + await uiSettings.set({ 'searchInferenceEndpoints:modelSettingsEnabled': true }); + }); + + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsPrivilegedUser(); + await pageObjects.featureSettings.goto(); + }); + + test.afterAll(async ({ uiSettings }) => { + await uiSettings.unset('searchInferenceEndpoints:modelSettingsEnabled'); + }); + + test('page header is visible', async ({ pageObjects }) => { + await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); + }); +}); From ca4140d45cbe4a13a2742d74a9ad498b143d9fd6 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 10:03:12 -0600 Subject: [PATCH 02/34] Refactor code --- .../page_objects/feature_settings_page.ts | 40 +++++++++++++++++++ .../scout/ui/tests/feature_settings.spec.ts | 40 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index 7a6d71490c2fb..17fded117be7a 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -15,7 +15,47 @@ export class FeatureSettingsPage { await this.page.testSubj.waitForSelector('modelSettingsPage'); } + // --- Header --- + public get pageHeader(): Locator { return this.page.testSubj.locator('modelSettingsPageHeader'); } + + public get saveButton(): Locator { + return this.page.testSubj.locator('save-settings-button'); + } + + public get apiDocumentationLink(): Locator { + return this.page.testSubj.locator('settings-api-documentation'); + } + + // --- Content --- + + public get content(): Locator { + return this.page.testSubj.locator('modelSettingsContent'); + } + + public get noFeaturesEmptyPrompt(): Locator { + return this.page.testSubj.locator('settings-no-features'); + } + + // --- Default Model Section --- + + public get defaultModelSection(): Locator { + return this.page.testSubj.locator('defaultModelSection'); + } + + public get defaultModelComboBox(): Locator { + return this.page.testSubj.locator('defaultModelComboBox'); + } + + public get disallowOtherModelsCheckbox(): Locator { + return this.page.testSubj.locator('disallowOtherModelsCheckbox'); + } + + // --- Dynamic Section Locators --- + + public getFeatureSection(parentName: string): Locator { + return this.page.testSubj.locator(`featureSection-${parentName}`); + } } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts index 18943d5d5f3e3..4fa9a396d0313 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts @@ -26,4 +26,44 @@ test.describe('Feature Settings', { tag: tags.deploymentAgnostic }, () => { test('page header is visible', async ({ pageObjects }) => { await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); }); + + test('page loads with default model section and controls', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('header controls are present', async () => { + await expect(featureSettings.saveButton).toBeVisible(); + await expect(featureSettings.saveButton).toBeDisabled(); + await expect(featureSettings.apiDocumentationLink).toBeVisible(); + }); + + await test.step('default model section is visible', async () => { + await expect(featureSettings.defaultModelSection).toBeVisible(); + await expect(featureSettings.defaultModelComboBox).toBeVisible(); + await expect(featureSettings.disallowOtherModelsCheckbox).toBeVisible(); + }); + }); + + test('disallow other models hides feature sections', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('enable disallow other models', async () => { + await featureSettings.disallowOtherModelsCheckbox.click(); + }); + + await test.step('feature sections are hidden', async () => { + await expect( + featureSettings.content.locator('[data-test-subj^="featureSection-"]') + ).toHaveCount(0); + }); + + await test.step('disable disallow other models', async () => { + await featureSettings.disallowOtherModelsCheckbox.click(); + }); + + await test.step('feature sections reappear', async () => { + await expect( + featureSettings.content.locator('[data-test-subj^="featureSection-"]') + ).not.toHaveCount(0); + }); + }); }); From e30d1807cf1e896f14d5988444069eb5bda4b717 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 10:21:59 -0600 Subject: [PATCH 03/34] Add tests for add-model --- .../page_objects/feature_settings_page.ts | 46 ++++++++++++++++++- .../scout/ui/tests/feature_settings.spec.ts | 42 ++++++++++++++--- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index 17fded117be7a..b3706ed7a2546 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -53,9 +53,53 @@ export class FeatureSettingsPage { return this.page.testSubj.locator('disallowOtherModelsCheckbox'); } - // --- Dynamic Section Locators --- + // --- Feature Sections --- public getFeatureSection(parentName: string): Locator { return this.page.testSubj.locator(`featureSection-${parentName}`); } + + public get allFeatureSections(): Locator { + return this.content.locator('[data-test-subj^="featureSection-"]'); + } + + public getResetLink(parentName: string): Locator { + return this.page.testSubj.locator(`reset-${parentName}`); + } + + // --- Sub-Feature Cards --- + + public getSubFeatureCard(featureId: string): Locator { + return this.page.testSubj.locator(`subFeatureCard-${featureId}`); + } + + public get allSubFeatureCards(): Locator { + return this.content.locator('[data-test-subj^="subFeatureCard-"]'); + } + + public get allEndpointRows(): Locator { + return this.content.locator('[data-test-subj^="endpoint-row-"]'); + } + + public getEndpointRow(endpointId: string): Locator { + return this.page.testSubj.locator(`endpoint-row-${endpointId}`); + } + + public getRemoveEndpointButton(endpointId: string): Locator { + return this.page.testSubj.locator(`remove-endpoint-${endpointId}`); + } + + public get disabledRemoveButtons(): Locator { + return this.content.locator('[data-test-subj^="remove-endpoint-"]:disabled'); + } + + // --- Add Model Popover --- + + public get firstAddModelButton(): Locator { + return this.content.locator('[data-test-subj="add-model-button"]').locator('nth=0'); + } + + public get addModelSearch(): Locator { + return this.page.testSubj.locator('add-model-search'); + } } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts index 4fa9a396d0313..5de28279d3f76 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts @@ -51,9 +51,7 @@ test.describe('Feature Settings', { tag: tags.deploymentAgnostic }, () => { }); await test.step('feature sections are hidden', async () => { - await expect( - featureSettings.content.locator('[data-test-subj^="featureSection-"]') - ).toHaveCount(0); + await expect(featureSettings.allFeatureSections).toHaveCount(0); }); await test.step('disable disallow other models', async () => { @@ -61,9 +59,41 @@ test.describe('Feature Settings', { tag: tags.deploymentAgnostic }, () => { }); await test.step('feature sections reappear', async () => { - await expect( - featureSettings.content.locator('[data-test-subj^="featureSection-"]') - ).not.toHaveCount(0); + await expect(featureSettings.allFeatureSections).not.toHaveCount(0); + }); + }); + + test('feature sections render with sub-feature cards', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('at least one feature section is visible', async () => { + await expect(featureSettings.allFeatureSections).not.toHaveCount(0); + }); + + await test.step('sub-feature cards are present with endpoint rows', async () => { + await expect(featureSettings.allSubFeatureCards).not.toHaveCount(0); + await expect(featureSettings.allEndpointRows).not.toHaveCount(0); + }); + + await test.step('endpoint rows contain a default badge', async () => { + await expect(featureSettings.content).toContainText('Default'); + }); + }); + + test('add model popover opens with search', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('open add model popover on a sub-feature', async () => { + await featureSettings.firstAddModelButton.click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + }); + }); + + test('remove button is disabled when only one endpoint remains', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('single-endpoint card has disabled remove button', async () => { + await expect(featureSettings.disabledRemoveButtons).not.toHaveCount(0); }); }); }); From dbd8fb5e04f5278756a293464f6361109dfb2034 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 10:26:31 -0600 Subject: [PATCH 04/34] Add tests for copy-to --- .../page_objects/feature_settings_page.ts | 36 +++++++++++++++++++ .../scout/ui/tests/feature_settings.spec.ts | 30 ++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index b3706ed7a2546..b80017c88d78c 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -102,4 +102,40 @@ export class FeatureSettingsPage { public get addModelSearch(): Locator { return this.page.testSubj.locator('add-model-search'); } + + // --- Copy To Modal --- + + public getCopyToButton(featureId: string): Locator { + return this.page.testSubj.locator(`copy-to-${featureId}`); + } + + public get firstCopyToButton(): Locator { + return this.content.locator('[data-test-subj^="copy-to-"]').locator('nth=0'); + } + + public get copyToModalApply(): Locator { + return this.page.testSubj.locator('copy-to-modal-apply'); + } + + public get copyToModalCancel(): Locator { + return this.page.testSubj.locator('copy-to-modal-cancel'); + } + + // --- Reset Defaults Modal --- + + public get firstResetLink(): Locator { + return this.content.locator('[data-test-subj^="reset-"]').locator('nth=0'); + } + + public get resetDefaultsModal(): Locator { + return this.page.testSubj.locator('resetDefaultsModal'); + } + + public get resetDefaultsConfirmButton(): Locator { + return this.resetDefaultsModal.locator('[data-test-subj="confirmModalConfirmButton"]'); + } + + public get resetDefaultsCancelButton(): Locator { + return this.resetDefaultsModal.locator('[data-test-subj="confirmModalCancelButton"]'); + } } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts index 5de28279d3f76..b1b0363e02523 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts @@ -96,4 +96,34 @@ test.describe('Feature Settings', { tag: tags.deploymentAgnostic }, () => { await expect(featureSettings.disabledRemoveButtons).not.toHaveCount(0); }); }); + + test('reset to defaults modal', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('click reset link opens confirmation modal', async () => { + await featureSettings.firstResetLink.click(); + await expect(featureSettings.resetDefaultsModal).toBeVisible(); + }); + + await test.step('cancel closes the modal without changes', async () => { + await featureSettings.resetDefaultsCancelButton.click(); + await expect(featureSettings.resetDefaultsModal).toBeHidden(); + await expect(featureSettings.saveButton).toBeDisabled(); + }); + }); + + test('copy to modal', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('click copy to opens modal', async () => { + await featureSettings.firstCopyToButton.click(); + await expect(featureSettings.copyToModalApply).toBeVisible(); + await expect(featureSettings.copyToModalApply).toBeDisabled(); + }); + + await test.step('cancel closes the modal', async () => { + await featureSettings.copyToModalCancel.click(); + await expect(featureSettings.copyToModalApply).toBeHidden(); + }); + }); }); From 0b689dede474ce4d51b851a71df2f8ee2a4d8bdb Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 10:35:38 -0600 Subject: [PATCH 05/34] Refactor code --- .../test/scout/ui/fixtures/constants.ts | 17 ----------------- .../test/scout/ui/fixtures/index.ts | 2 -- 2 files changed, 19 deletions(-) delete mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/constants.ts diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/constants.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/constants.ts deleted file mode 100644 index 45a9f1c9c3e6e..0000000000000 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/constants.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export const DEFAULT_START_TIME = '2020-01-01T00:00:00.000Z'; -export const DEFAULT_END_TIME = '2020-01-02T00:00:00.000Z'; - -export const ES_ARCHIVES = { - SOME_ARCHIVE: 'path/to/es_archive', -}; - -export const KBN_ARCHIVES = { - SOME_ARCHIVE: 'path/to/kbn_archive', -}; diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/index.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/index.ts index 6ae694e8537ca..1a75e4ecbea34 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/index.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/index.ts @@ -34,5 +34,3 @@ export const test = baseTest.extend({ await use(extendedPageObjects); }, }); - -export * as testData from './constants'; From 895df279dcde2518902962bbd8ce74166606bf10 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 10:42:52 -0600 Subject: [PATCH 06/34] Refactor code --- .../page_objects/feature_settings_page.ts | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index b80017c88d78c..b0dce5053eb00 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -35,10 +35,6 @@ export class FeatureSettingsPage { return this.page.testSubj.locator('modelSettingsContent'); } - public get noFeaturesEmptyPrompt(): Locator { - return this.page.testSubj.locator('settings-no-features'); - } - // --- Default Model Section --- public get defaultModelSection(): Locator { @@ -55,24 +51,12 @@ export class FeatureSettingsPage { // --- Feature Sections --- - public getFeatureSection(parentName: string): Locator { - return this.page.testSubj.locator(`featureSection-${parentName}`); - } - public get allFeatureSections(): Locator { return this.content.locator('[data-test-subj^="featureSection-"]'); } - public getResetLink(parentName: string): Locator { - return this.page.testSubj.locator(`reset-${parentName}`); - } - // --- Sub-Feature Cards --- - public getSubFeatureCard(featureId: string): Locator { - return this.page.testSubj.locator(`subFeatureCard-${featureId}`); - } - public get allSubFeatureCards(): Locator { return this.content.locator('[data-test-subj^="subFeatureCard-"]'); } @@ -81,14 +65,6 @@ export class FeatureSettingsPage { return this.content.locator('[data-test-subj^="endpoint-row-"]'); } - public getEndpointRow(endpointId: string): Locator { - return this.page.testSubj.locator(`endpoint-row-${endpointId}`); - } - - public getRemoveEndpointButton(endpointId: string): Locator { - return this.page.testSubj.locator(`remove-endpoint-${endpointId}`); - } - public get disabledRemoveButtons(): Locator { return this.content.locator('[data-test-subj^="remove-endpoint-"]:disabled'); } @@ -105,10 +81,6 @@ export class FeatureSettingsPage { // --- Copy To Modal --- - public getCopyToButton(featureId: string): Locator { - return this.page.testSubj.locator(`copy-to-${featureId}`); - } - public get firstCopyToButton(): Locator { return this.content.locator('[data-test-subj^="copy-to-"]').locator('nth=0'); } @@ -131,10 +103,6 @@ export class FeatureSettingsPage { return this.page.testSubj.locator('resetDefaultsModal'); } - public get resetDefaultsConfirmButton(): Locator { - return this.resetDefaultsModal.locator('[data-test-subj="confirmModalConfirmButton"]'); - } - public get resetDefaultsCancelButton(): Locator { return this.resetDefaultsModal.locator('[data-test-subj="confirmModalCancelButton"]'); } From ad09b7a050d2bfe7e8f46ec81985c07e0e6a3967 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 10:48:59 -0600 Subject: [PATCH 07/34] Refactor code --- .../ui/fixtures/page_objects/feature_settings_page.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index b0dce5053eb00..e5d9842f8ca4b 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -72,7 +72,8 @@ export class FeatureSettingsPage { // --- Add Model Popover --- public get firstAddModelButton(): Locator { - return this.content.locator('[data-test-subj="add-model-button"]').locator('nth=0'); + // eslint-disable-next-line playwright/no-nth-methods + return this.content.locator('[data-test-subj="add-model-button"]').first(); } public get addModelSearch(): Locator { @@ -82,7 +83,8 @@ export class FeatureSettingsPage { // --- Copy To Modal --- public get firstCopyToButton(): Locator { - return this.content.locator('[data-test-subj^="copy-to-"]').locator('nth=0'); + // eslint-disable-next-line playwright/no-nth-methods + return this.content.locator('[data-test-subj^="copy-to-"]').first(); } public get copyToModalApply(): Locator { @@ -96,7 +98,8 @@ export class FeatureSettingsPage { // --- Reset Defaults Modal --- public get firstResetLink(): Locator { - return this.content.locator('[data-test-subj^="reset-"]').locator('nth=0'); + // eslint-disable-next-line playwright/no-nth-methods + return this.content.locator('[data-test-subj^="reset-"]').first(); } public get resetDefaultsModal(): Locator { From 67b38efc391c749e21d93493dd9524e251e60ad1 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 11:11:22 -0600 Subject: [PATCH 08/34] Refactor code --- .../page_objects/feature_settings_page.ts | 34 +++++++++++++++--- .../scout/ui/tests/feature_settings.spec.ts | 35 +++++++++++-------- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index e5d9842f8ca4b..a9ea912cb2283 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -5,7 +5,9 @@ * 2.0. */ +/* eslint-disable playwright/no-nth-methods */ import type { ScoutPage, Locator } from '@kbn/scout'; +import { expect } from '@kbn/scout/ui'; export class FeatureSettingsPage { constructor(private readonly page: ScoutPage) {} @@ -65,14 +67,13 @@ export class FeatureSettingsPage { return this.content.locator('[data-test-subj^="endpoint-row-"]'); } - public get disabledRemoveButtons(): Locator { - return this.content.locator('[data-test-subj^="remove-endpoint-"]:disabled'); + public get firstEndpointRow(): Locator { + return this.allEndpointRows.first(); } // --- Add Model Popover --- public get firstAddModelButton(): Locator { - // eslint-disable-next-line playwright/no-nth-methods return this.content.locator('[data-test-subj="add-model-button"]').first(); } @@ -83,7 +84,6 @@ export class FeatureSettingsPage { // --- Copy To Modal --- public get firstCopyToButton(): Locator { - // eslint-disable-next-line playwright/no-nth-methods return this.content.locator('[data-test-subj^="copy-to-"]').first(); } @@ -98,7 +98,6 @@ export class FeatureSettingsPage { // --- Reset Defaults Modal --- public get firstResetLink(): Locator { - // eslint-disable-next-line playwright/no-nth-methods return this.content.locator('[data-test-subj^="reset-"]').first(); } @@ -106,7 +105,32 @@ export class FeatureSettingsPage { return this.page.testSubj.locator('resetDefaultsModal'); } + public get resetDefaultsConfirmButton(): Locator { + return this.resetDefaultsModal.locator('[data-test-subj="confirmModalConfirmButton"]'); + } + public get resetDefaultsCancelButton(): Locator { return this.resetDefaultsModal.locator('[data-test-subj="confirmModalCancelButton"]'); } + + /** + * Removes endpoints from the first sub-feature card until only one remains. + * Returns the initial count of endpoints before removal. + */ + public async removeEndpointsUntilOneRemains(): Promise { + const firstCard = this.allSubFeatureCards.first(); + const removeButtons = firstCard.locator('[data-test-subj^="remove-endpoint-"]'); + const initialCount = await removeButtons.count(); + + for (let remaining = initialCount; remaining > 1; remaining--) { + await removeButtons.last().click(); + await expect(removeButtons).toHaveCount(remaining - 1); + } + + return initialCount; + } + + public get firstCardLastRemoveButton(): Locator { + return this.allSubFeatureCards.first().locator('[data-test-subj^="remove-endpoint-"]').last(); + } } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts index b1b0363e02523..1ac0a94e87616 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts @@ -10,19 +10,11 @@ import { expect } from '@kbn/scout/ui'; import { test } from '../fixtures'; test.describe('Feature Settings', { tag: tags.deploymentAgnostic }, () => { - test.beforeAll(async ({ uiSettings }) => { - await uiSettings.set({ 'searchInferenceEndpoints:modelSettingsEnabled': true }); - }); - test.beforeEach(async ({ browserAuth, pageObjects }) => { await browserAuth.loginAsPrivilegedUser(); await pageObjects.featureSettings.goto(); }); - test.afterAll(async ({ uiSettings }) => { - await uiSettings.unset('searchInferenceEndpoints:modelSettingsEnabled'); - }); - test('page header is visible', async ({ pageObjects }) => { await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); }); @@ -75,8 +67,8 @@ test.describe('Feature Settings', { tag: tags.deploymentAgnostic }, () => { await expect(featureSettings.allEndpointRows).not.toHaveCount(0); }); - await test.step('endpoint rows contain a default badge', async () => { - await expect(featureSettings.content).toContainText('Default'); + await test.step('first endpoint row has a default badge', async () => { + await expect(featureSettings.firstEndpointRow).toContainText('Default'); }); }); @@ -89,15 +81,30 @@ test.describe('Feature Settings', { tag: tags.deploymentAgnostic }, () => { }); }); - test('remove button is disabled when only one endpoint remains', async ({ pageObjects }) => { + test('remove endpoints until one remains then reset to defaults', async ({ pageObjects }) => { const { featureSettings } = pageObjects; - await test.step('single-endpoint card has disabled remove button', async () => { - await expect(featureSettings.disabledRemoveButtons).not.toHaveCount(0); + await test.step('remove endpoints until only one remains', async () => { + await featureSettings.removeEndpointsUntilOneRemains(); + }); + + await test.step('last remaining remove button is disabled', async () => { + await expect(featureSettings.firstCardLastRemoveButton).toBeDisabled(); + }); + + await test.step('reset section to defaults', async () => { + await featureSettings.firstResetLink.click(); + await expect(featureSettings.resetDefaultsModal).toBeVisible(); + await featureSettings.resetDefaultsConfirmButton.click(); + await expect(featureSettings.resetDefaultsModal).toBeHidden(); + }); + + await test.step('save button is disabled after reset', async () => { + await expect(featureSettings.saveButton).toBeDisabled(); }); }); - test('reset to defaults modal', async ({ pageObjects }) => { + test('reset to defaults modal cancel', async ({ pageObjects }) => { const { featureSettings } = pageObjects; await test.step('click reset link opens confirmation modal', async () => { From c8cbf229e83458594345587521523749c8ffbc1a Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 12:56:41 -0600 Subject: [PATCH 09/34] Refactor code --- .../ui/fixtures/page_objects/feature_settings_page.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index a9ea912cb2283..33751b8211d2b 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -7,7 +7,6 @@ /* eslint-disable playwright/no-nth-methods */ import type { ScoutPage, Locator } from '@kbn/scout'; -import { expect } from '@kbn/scout/ui'; export class FeatureSettingsPage { constructor(private readonly page: ScoutPage) {} @@ -119,12 +118,13 @@ export class FeatureSettingsPage { */ public async removeEndpointsUntilOneRemains(): Promise { const firstCard = this.allSubFeatureCards.first(); - const removeButtons = firstCard.locator('[data-test-subj^="remove-endpoint-"]'); - const initialCount = await removeButtons.count(); + const endpointRows = firstCard.locator('[data-test-subj^="endpoint-row-"]'); + const initialCount = await endpointRows.count(); for (let remaining = initialCount; remaining > 1; remaining--) { - await removeButtons.last().click(); - await expect(removeButtons).toHaveCount(remaining - 1); + const lastRemoveBtn = firstCard.locator('[data-test-subj^="remove-endpoint-"]').last(); + await lastRemoveBtn.click(); + await lastRemoveBtn.waitFor({ state: 'detached' }); } return initialCount; From 1cfea435f46017190caf5c49220244736cf4c4e2 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 14:07:53 -0600 Subject: [PATCH 10/34] Refactor code --- .../plugins/shared/search_inference_endpoints/tsconfig.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json b/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json index 7954a92cddfb5..7a881be72c679 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json @@ -7,7 +7,8 @@ "__mocks__/**/*", "common/**/*", "public/**/*", - "server/**/*" + "server/**/*", + "test/scout/**/*" ], "kbn_references": [ "@kbn/config-schema", @@ -45,7 +46,8 @@ "@kbn/core-security-server", "@kbn/inference-common", "@kbn/inference-plugin", - "@kbn/management-settings-ids" + "@kbn/management-settings-ids", + "@kbn/scout" ], "exclude": [ "target/**/*" From 3cecb115bffb7df1fad3718577d381a8ff5f07ac Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 14:15:29 -0600 Subject: [PATCH 11/34] Refactor code --- .../platform/plugins/shared/search_inference_endpoints/moon.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml b/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml index b3f5d262cb464..6adf0379722a3 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml @@ -53,6 +53,7 @@ dependsOn: - '@kbn/inference-common' - '@kbn/inference-plugin' - '@kbn/management-settings-ids' + - '@kbn/scout' tags: - plugin - prod @@ -65,6 +66,7 @@ fileGroups: - common/**/* - public/**/* - server/**/* + - test/scout/**/* - '!target/**/*' jest-config: - jest.config.js From b0cf1161baf2bf18bed178f6e449c002be2c2e9e Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 15:49:07 -0600 Subject: [PATCH 12/34] Refactor code --- .../scout/ui/tests/feature_settings.spec.ts | 190 +++++++++--------- 1 file changed, 97 insertions(+), 93 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts index 1ac0a94e87616..15c83f4b3eb03 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts @@ -9,128 +9,132 @@ import { tags } from '@kbn/scout'; import { expect } from '@kbn/scout/ui'; import { test } from '../fixtures'; -test.describe('Feature Settings', { tag: tags.deploymentAgnostic }, () => { - test.beforeEach(async ({ browserAuth, pageObjects }) => { - await browserAuth.loginAsPrivilegedUser(); - await pageObjects.featureSettings.goto(); - }); - - test('page header is visible', async ({ pageObjects }) => { - await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); - }); - - test('page loads with default model section and controls', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('header controls are present', async () => { - await expect(featureSettings.saveButton).toBeVisible(); - await expect(featureSettings.saveButton).toBeDisabled(); - await expect(featureSettings.apiDocumentationLink).toBeVisible(); +test.describe( + 'Feature Settings', + { tag: [...tags.stateful.classic, ...tags.serverless.search] }, + () => { + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsPrivilegedUser(); + await pageObjects.featureSettings.goto(); }); - await test.step('default model section is visible', async () => { - await expect(featureSettings.defaultModelSection).toBeVisible(); - await expect(featureSettings.defaultModelComboBox).toBeVisible(); - await expect(featureSettings.disallowOtherModelsCheckbox).toBeVisible(); + test('page header is visible', async ({ pageObjects }) => { + await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); }); - }); - test('disallow other models hides feature sections', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + test('page loads with default model section and controls', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('enable disallow other models', async () => { - await featureSettings.disallowOtherModelsCheckbox.click(); - }); + await test.step('header controls are present', async () => { + await expect(featureSettings.saveButton).toBeVisible(); + await expect(featureSettings.saveButton).toBeDisabled(); + await expect(featureSettings.apiDocumentationLink).toBeVisible(); + }); - await test.step('feature sections are hidden', async () => { - await expect(featureSettings.allFeatureSections).toHaveCount(0); + await test.step('default model section is visible', async () => { + await expect(featureSettings.defaultModelSection).toBeVisible(); + await expect(featureSettings.defaultModelComboBox).toBeVisible(); + await expect(featureSettings.disallowOtherModelsCheckbox).toBeVisible(); + }); }); - await test.step('disable disallow other models', async () => { - await featureSettings.disallowOtherModelsCheckbox.click(); - }); + test('disallow other models hides feature sections', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('feature sections reappear', async () => { - await expect(featureSettings.allFeatureSections).not.toHaveCount(0); - }); - }); + await test.step('enable disallow other models', async () => { + await featureSettings.disallowOtherModelsCheckbox.click(); + }); - test('feature sections render with sub-feature cards', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + await test.step('feature sections are hidden', async () => { + await expect(featureSettings.allFeatureSections).toHaveCount(0); + }); - await test.step('at least one feature section is visible', async () => { - await expect(featureSettings.allFeatureSections).not.toHaveCount(0); - }); + await test.step('disable disallow other models', async () => { + await featureSettings.disallowOtherModelsCheckbox.click(); + }); - await test.step('sub-feature cards are present with endpoint rows', async () => { - await expect(featureSettings.allSubFeatureCards).not.toHaveCount(0); - await expect(featureSettings.allEndpointRows).not.toHaveCount(0); + await test.step('feature sections reappear', async () => { + await expect(featureSettings.allFeatureSections).not.toHaveCount(0); + }); }); - await test.step('first endpoint row has a default badge', async () => { - await expect(featureSettings.firstEndpointRow).toContainText('Default'); - }); - }); + test('feature sections render with sub-feature cards', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('at least one feature section is visible', async () => { + await expect(featureSettings.allFeatureSections).not.toHaveCount(0); + }); - test('add model popover opens with search', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + await test.step('sub-feature cards are present with endpoint rows', async () => { + await expect(featureSettings.allSubFeatureCards).not.toHaveCount(0); + await expect(featureSettings.allEndpointRows).not.toHaveCount(0); + }); - await test.step('open add model popover on a sub-feature', async () => { - await featureSettings.firstAddModelButton.click(); - await expect(featureSettings.addModelSearch).toBeVisible(); + await test.step('first endpoint row has a default badge', async () => { + await expect(featureSettings.firstEndpointRow).toContainText('Default'); + }); }); - }); - test('remove endpoints until one remains then reset to defaults', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + test('add model popover opens with search', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('remove endpoints until only one remains', async () => { - await featureSettings.removeEndpointsUntilOneRemains(); + await test.step('open add model popover on a sub-feature', async () => { + await featureSettings.firstAddModelButton.click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + }); }); - await test.step('last remaining remove button is disabled', async () => { - await expect(featureSettings.firstCardLastRemoveButton).toBeDisabled(); - }); + test('remove endpoints until one remains then reset to defaults', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('reset section to defaults', async () => { - await featureSettings.firstResetLink.click(); - await expect(featureSettings.resetDefaultsModal).toBeVisible(); - await featureSettings.resetDefaultsConfirmButton.click(); - await expect(featureSettings.resetDefaultsModal).toBeHidden(); - }); + await test.step('remove endpoints until only one remains', async () => { + await featureSettings.removeEndpointsUntilOneRemains(); + }); - await test.step('save button is disabled after reset', async () => { - await expect(featureSettings.saveButton).toBeDisabled(); - }); - }); + await test.step('last remaining remove button is disabled', async () => { + await expect(featureSettings.firstCardLastRemoveButton).toBeDisabled(); + }); - test('reset to defaults modal cancel', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + await test.step('reset section to defaults', async () => { + await featureSettings.firstResetLink.click(); + await expect(featureSettings.resetDefaultsModal).toBeVisible(); + await featureSettings.resetDefaultsConfirmButton.click(); + await expect(featureSettings.resetDefaultsModal).toBeHidden(); + }); - await test.step('click reset link opens confirmation modal', async () => { - await featureSettings.firstResetLink.click(); - await expect(featureSettings.resetDefaultsModal).toBeVisible(); + await test.step('save button is disabled after reset', async () => { + await expect(featureSettings.saveButton).toBeDisabled(); + }); }); - await test.step('cancel closes the modal without changes', async () => { - await featureSettings.resetDefaultsCancelButton.click(); - await expect(featureSettings.resetDefaultsModal).toBeHidden(); - await expect(featureSettings.saveButton).toBeDisabled(); - }); - }); + test('reset to defaults modal cancel', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - test('copy to modal', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + await test.step('click reset link opens confirmation modal', async () => { + await featureSettings.firstResetLink.click(); + await expect(featureSettings.resetDefaultsModal).toBeVisible(); + }); - await test.step('click copy to opens modal', async () => { - await featureSettings.firstCopyToButton.click(); - await expect(featureSettings.copyToModalApply).toBeVisible(); - await expect(featureSettings.copyToModalApply).toBeDisabled(); + await test.step('cancel closes the modal without changes', async () => { + await featureSettings.resetDefaultsCancelButton.click(); + await expect(featureSettings.resetDefaultsModal).toBeHidden(); + await expect(featureSettings.saveButton).toBeDisabled(); + }); }); - await test.step('cancel closes the modal', async () => { - await featureSettings.copyToModalCancel.click(); - await expect(featureSettings.copyToModalApply).toBeHidden(); + test('copy to modal', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('click copy to opens modal', async () => { + await featureSettings.firstCopyToButton.click(); + await expect(featureSettings.copyToModalApply).toBeVisible(); + await expect(featureSettings.copyToModalApply).toBeDisabled(); + }); + + await test.step('cancel closes the modal', async () => { + await featureSettings.copyToModalCancel.click(); + await expect(featureSettings.copyToModalApply).toBeHidden(); + }); }); - }); -}); + } +); From 0e699be9a600d5cb95ef9bd055f92e1b338b75f7 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Tue, 7 Apr 2026 16:29:52 -0600 Subject: [PATCH 13/34] Refactor code --- .../page_objects/feature_settings_page.ts | 26 ------------------- .../scout/ui/tests/feature_settings.spec.ts | 23 ---------------- 2 files changed, 49 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index 33751b8211d2b..a8e290eb87eea 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -104,33 +104,7 @@ export class FeatureSettingsPage { return this.page.testSubj.locator('resetDefaultsModal'); } - public get resetDefaultsConfirmButton(): Locator { - return this.resetDefaultsModal.locator('[data-test-subj="confirmModalConfirmButton"]'); - } - public get resetDefaultsCancelButton(): Locator { return this.resetDefaultsModal.locator('[data-test-subj="confirmModalCancelButton"]'); } - - /** - * Removes endpoints from the first sub-feature card until only one remains. - * Returns the initial count of endpoints before removal. - */ - public async removeEndpointsUntilOneRemains(): Promise { - const firstCard = this.allSubFeatureCards.first(); - const endpointRows = firstCard.locator('[data-test-subj^="endpoint-row-"]'); - const initialCount = await endpointRows.count(); - - for (let remaining = initialCount; remaining > 1; remaining--) { - const lastRemoveBtn = firstCard.locator('[data-test-subj^="remove-endpoint-"]').last(); - await lastRemoveBtn.click(); - await lastRemoveBtn.waitFor({ state: 'detached' }); - } - - return initialCount; - } - - public get firstCardLastRemoveButton(): Locator { - return this.allSubFeatureCards.first().locator('[data-test-subj^="remove-endpoint-"]').last(); - } } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts index 15c83f4b3eb03..2dff3d87e7f01 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts @@ -84,29 +84,6 @@ test.describe( }); }); - test('remove endpoints until one remains then reset to defaults', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('remove endpoints until only one remains', async () => { - await featureSettings.removeEndpointsUntilOneRemains(); - }); - - await test.step('last remaining remove button is disabled', async () => { - await expect(featureSettings.firstCardLastRemoveButton).toBeDisabled(); - }); - - await test.step('reset section to defaults', async () => { - await featureSettings.firstResetLink.click(); - await expect(featureSettings.resetDefaultsModal).toBeVisible(); - await featureSettings.resetDefaultsConfirmButton.click(); - await expect(featureSettings.resetDefaultsModal).toBeHidden(); - }); - - await test.step('save button is disabled after reset', async () => { - await expect(featureSettings.saveButton).toBeDisabled(); - }); - }); - test('reset to defaults modal cancel', async ({ pageObjects }) => { const { featureSettings } = pageObjects; From 77a545e1c825d3d5c919355bb2db225d9606117a Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 07:25:46 -0600 Subject: [PATCH 14/34] Add comment for each line of disable next-line --- .../scout/ui/fixtures/page_objects/feature_settings_page.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index a8e290eb87eea..f689584e3ee5f 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -5,7 +5,6 @@ * 2.0. */ -/* eslint-disable playwright/no-nth-methods */ import type { ScoutPage, Locator } from '@kbn/scout'; export class FeatureSettingsPage { @@ -67,12 +66,14 @@ export class FeatureSettingsPage { } public get firstEndpointRow(): Locator { + // eslint-disable-next-line playwright/no-nth-methods -- selecting the first endpoint row to verify default badge presence return this.allEndpointRows.first(); } // --- Add Model Popover --- public get firstAddModelButton(): Locator { + // eslint-disable-next-line playwright/no-nth-methods -- multiple sub-features have add-model buttons; picking the first to test the popover flow return this.content.locator('[data-test-subj="add-model-button"]').first(); } @@ -83,6 +84,7 @@ export class FeatureSettingsPage { // --- Copy To Modal --- public get firstCopyToButton(): Locator { + // eslint-disable-next-line playwright/no-nth-methods -- multiple sub-features have copy-to buttons; picking the first to test the modal flow return this.content.locator('[data-test-subj^="copy-to-"]').first(); } @@ -97,6 +99,7 @@ export class FeatureSettingsPage { // --- Reset Defaults Modal --- public get firstResetLink(): Locator { + // eslint-disable-next-line playwright/no-nth-methods -- multiple sub-features have reset links; picking the first to test the modal flow return this.content.locator('[data-test-subj^="reset-"]').first(); } From 3d7953e0914e23d4775c245ec174beaf590a6b37 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 08:01:14 -0600 Subject: [PATCH 15/34] Mock inference api --- .../fixtures/mock_data/inference_endpoints.ts | 69 +++++++++++++++++++ .../page_objects/feature_settings_page.ts | 18 +++++ .../scout/ui/tests/feature_settings.spec.ts | 54 +++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts new file mode 100644 index 0000000000000..bb614c45837fd --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts @@ -0,0 +1,69 @@ +/* + * 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. + */ + +export const mockInferenceEndpoints = [ + { + inference_id: '.anthropic-claude-3.7-sonnet-chat_completion', + task_type: 'chat_completion', + service: 'elastic', + service_settings: { model_id: 'anthropic-claude-3.7-sonnet' }, + metadata: { + heuristics: { properties: ['multilingual', 'multimodal'], status: 'ga' }, + display: { name: 'Anthropic Claude Sonnet 3.7', model_creator: 'Anthropic' }, + }, + }, + { + inference_id: '.anthropic-claude-3.7-sonnet-completion', + task_type: 'completion', + service: 'elastic', + service_settings: { model_id: 'anthropic-claude-3.7-sonnet' }, + metadata: { + heuristics: { properties: ['multilingual', 'multimodal'], status: 'ga' }, + display: { name: 'Anthropic Claude Sonnet 3.7', model_creator: 'Anthropic' }, + }, + }, + { + inference_id: '.openai-gpt-4.1-chat_completion', + task_type: 'chat_completion', + service: 'elastic', + service_settings: { model_id: 'openai-gpt-4.1' }, + metadata: { + heuristics: { properties: ['multilingual', 'multimodal'], status: 'ga' }, + display: { name: 'OpenAI GPT-4.1', model_creator: 'OpenAI' }, + }, + }, + { + inference_id: '.openai-gpt-4.1-completion', + task_type: 'completion', + service: 'elastic', + service_settings: { model_id: 'openai-gpt-4.1' }, + metadata: { + heuristics: { properties: ['multilingual', 'multimodal'], status: 'ga' }, + display: { name: 'OpenAI GPT-4.1', model_creator: 'OpenAI' }, + }, + }, + { + inference_id: '.google-gemini-2.5-pro-chat_completion', + task_type: 'chat_completion', + service: 'elastic', + service_settings: { model_id: 'google-gemini-2.5-pro' }, + metadata: { + heuristics: { properties: ['multilingual', 'multimodal'], status: 'ga' }, + display: { name: 'Google Gemini 2.5 Pro', model_creator: 'Google' }, + }, + }, + { + inference_id: '.google-gemini-2.5-pro-completion', + task_type: 'completion', + service: 'elastic', + service_settings: { model_id: 'google-gemini-2.5-pro' }, + metadata: { + heuristics: { properties: ['multilingual', 'multimodal'], status: 'ga' }, + display: { name: 'Google Gemini 2.5 Pro', model_creator: 'Google' }, + }, + }, +]; diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index f689584e3ee5f..1fd223820dac7 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -110,4 +110,22 @@ export class FeatureSettingsPage { public get resetDefaultsCancelButton(): Locator { return this.resetDefaultsModal.locator('[data-test-subj="confirmModalCancelButton"]'); } + + // --- Add Model Popover Options --- + + public get addModelOptions(): Locator { + return this.page.locator('li[role="option"]'); + } + + // --- Route Mocking --- + + public async mockInferenceEndpoints(endpoints: unknown[]) { + await this.page.route('**/internal/inference_endpoints/endpoints', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ inference_endpoints: endpoints }), + }); + }); + } } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts index 2dff3d87e7f01..2036621ba6ed1 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts @@ -8,6 +8,7 @@ import { tags } from '@kbn/scout'; import { expect } from '@kbn/scout/ui'; import { test } from '../fixtures'; +import { mockInferenceEndpoints } from '../fixtures/mock_data/inference_endpoints'; test.describe( 'Feature Settings', @@ -113,5 +114,58 @@ test.describe( await expect(featureSettings.copyToModalApply).toBeHidden(); }); }); + + test('add model popover search filters results', async ({ page, pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('mock inference endpoints with chat_completion models', async () => { + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await page.reload(); + await featureSettings.goto(); + }); + + await test.step('open popover and verify models are listed', async () => { + await featureSettings.firstAddModelButton.click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + await expect(featureSettings.addModelOptions).not.toHaveCount(0); + }); + + await test.step('search filters the model list', async () => { + await featureSettings.addModelSearch.fill('anthropic'); + await expect(featureSettings.addModelOptions).toHaveCount(1); + await expect(featureSettings.addModelOptions).toContainText('anthropic'); + }); + }); + + test('selecting a model adds it to the assigned models list', async ({ page, pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('mock inference endpoints', async () => { + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await page.reload(); + await featureSettings.goto(); + }); + + await test.step('count existing endpoint rows', async () => { + await expect(featureSettings.allEndpointRows).not.toHaveCount(0); + }); + + const initialCount = await featureSettings.allEndpointRows.count(); + + await test.step('select a model from the popover', async () => { + await featureSettings.firstAddModelButton.click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + // eslint-disable-next-line playwright/no-nth-methods -- selecting the first available model from the popover list + await featureSettings.addModelOptions.first().click(); + }); + + await test.step('endpoint row count increases', async () => { + await expect(featureSettings.allEndpointRows).toHaveCount(initialCount + 1); + }); + + await test.step('save button becomes enabled', async () => { + await expect(featureSettings.saveButton).toBeEnabled(); + }); + }); } ); From 3daabef72a6fa44d227cd3763d86f059c446a3c8 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 08:06:00 -0600 Subject: [PATCH 16/34] Refactor code --- .../public/components/settings/add_model_popover.tsx | 1 + .../scout/ui/fixtures/page_objects/feature_settings_page.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/add_model_popover.tsx b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/add_model_popover.tsx index f2fdf8ec21792..6a41d16ed3ab6 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/add_model_popover.tsx +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/public/components/settings/add_model_popover.tsx @@ -104,6 +104,7 @@ export const AddModelPopover: React.FC = ({ onChange={handleChange} singleSelection searchable + data-test-subj="add-model-selectable" searchProps={{ placeholder: i18n.translate('xpack.searchInferenceEndpoints.settings.addModel.search', { defaultMessage: 'Search models...', diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts index 1fd223820dac7..7a4e95741f881 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts @@ -114,7 +114,7 @@ export class FeatureSettingsPage { // --- Add Model Popover Options --- public get addModelOptions(): Locator { - return this.page.locator('li[role="option"]'); + return this.page.testSubj.locator('add-model-selectable').getByRole('option'); } // --- Route Mocking --- From ee311fb19ce0d2de9bdae0196d5d7bf4b63e68be Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 08:16:01 -0600 Subject: [PATCH 17/34] Refactor code --- .../ui/fixtures/mock_data/inference_endpoints.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts index bb614c45837fd..1fbde78620cf2 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts @@ -7,7 +7,7 @@ export const mockInferenceEndpoints = [ { - inference_id: '.anthropic-claude-3.7-sonnet-chat_completion', + inference_id: '.anthropic-claude-3.7-sonnet-chat_completion-a3f1', task_type: 'chat_completion', service: 'elastic', service_settings: { model_id: 'anthropic-claude-3.7-sonnet' }, @@ -17,7 +17,7 @@ export const mockInferenceEndpoints = [ }, }, { - inference_id: '.anthropic-claude-3.7-sonnet-completion', + inference_id: '.anthropic-claude-3.7-sonnet-completion-b7d2', task_type: 'completion', service: 'elastic', service_settings: { model_id: 'anthropic-claude-3.7-sonnet' }, @@ -27,7 +27,7 @@ export const mockInferenceEndpoints = [ }, }, { - inference_id: '.openai-gpt-4.1-chat_completion', + inference_id: '.openai-gpt-4.1-chat_completion-c9e4', task_type: 'chat_completion', service: 'elastic', service_settings: { model_id: 'openai-gpt-4.1' }, @@ -37,7 +37,7 @@ export const mockInferenceEndpoints = [ }, }, { - inference_id: '.openai-gpt-4.1-completion', + inference_id: '.openai-gpt-4.1-completion-d5f6', task_type: 'completion', service: 'elastic', service_settings: { model_id: 'openai-gpt-4.1' }, @@ -47,7 +47,7 @@ export const mockInferenceEndpoints = [ }, }, { - inference_id: '.google-gemini-2.5-pro-chat_completion', + inference_id: '.google-gemini-2.5-pro-chat_completion-e2a8', task_type: 'chat_completion', service: 'elastic', service_settings: { model_id: 'google-gemini-2.5-pro' }, @@ -57,7 +57,7 @@ export const mockInferenceEndpoints = [ }, }, { - inference_id: '.google-gemini-2.5-pro-completion', + inference_id: '.google-gemini-2.5-pro-completion-f1b3', task_type: 'completion', service: 'elastic', service_settings: { model_id: 'google-gemini-2.5-pro' }, From 9d9f7d3707a1bc176ef5c5fb4be7f0a86a5a7d98 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 12:28:19 -0600 Subject: [PATCH 18/34] Introduce fixture plugin --- .github/CODEOWNERS | 1 + package.json | 1 + .../stateful/classic.stateful.config.ts | 26 ++ tsconfig.base.json | 2 + .../scout/ui/tests/feature_settings.spec.ts | 171 ------------- .../ui/fixtures/index.ts | 0 .../fixtures/mock_data/inference_endpoints.ts | 12 +- .../page_objects/feature_settings_page.ts | 41 ++-- .../ui/fixtures/page_objects/index.ts | 0 .../ui/playwright.config.ts | 0 .../ui/tests/feature_settings.spec.ts | 225 ++++++++++++++++++ .../kibana.jsonc | 15 ++ .../moon.yml | 32 +++ .../package.json | 13 + .../server/index.ts | 13 + .../server/plugin.ts | 52 ++++ .../tsconfig.json | 18 ++ yarn.lock | 54 +---- 18 files changed, 439 insertions(+), 237 deletions(-) create mode 100644 src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/inference_test/stateful/classic.stateful.config.ts delete mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts rename x-pack/platform/plugins/shared/search_inference_endpoints/test/{scout => scout_inference_test}/ui/fixtures/index.ts (100%) rename x-pack/platform/plugins/shared/search_inference_endpoints/test/{scout => scout_inference_test}/ui/fixtures/mock_data/inference_endpoints.ts (84%) rename x-pack/platform/plugins/shared/search_inference_endpoints/test/{scout => scout_inference_test}/ui/fixtures/page_objects/feature_settings_page.ts (80%) rename x-pack/platform/plugins/shared/search_inference_endpoints/test/{scout => scout_inference_test}/ui/fixtures/page_objects/index.ts (100%) rename x-pack/platform/plugins/shared/search_inference_endpoints/test/{scout => scout_inference_test}/ui/playwright.config.ts (100%) create mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts create mode 100644 x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/kibana.jsonc create mode 100644 x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/moon.yml create mode 100644 x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/package.json create mode 100644 x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/server/index.ts create mode 100644 x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/server/plugin.ts create mode 100644 x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a300451b3fd6e..51da2ce576883 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1199,6 +1199,7 @@ x-pack/platform/test/plugin_api_perf/plugins/task_manager_performance @elastic/r x-pack/platform/test/reporting_api_integration/plugins/reporting_fixture @elastic/response-ops x-pack/platform/test/reporting_api_integration/plugins/reporting_test_routes @elastic/response-ops x-pack/platform/test/saved_object_api_integration/common/plugins/saved_object_test_plugin @elastic/kibana-security +x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture @elastic/search-kibana x-pack/platform/test/security_api_integration/packages/helpers @elastic/kibana-security x-pack/platform/test/security_api_integration/plugins/audit_log @elastic/kibana-security x-pack/platform/test/security_api_integration/plugins/features_provider @elastic/kibana-security diff --git a/package.json b/package.json index 3a81243f8c974..7173fe1462c0c 100644 --- a/package.json +++ b/package.json @@ -999,6 +999,7 @@ "@kbn/search-homepage": "link:x-pack/solutions/search/plugins/search_homepage", "@kbn/search-index-documents": "link:x-pack/platform/packages/shared/kbn-search-index-documents", "@kbn/search-inference-endpoints": "link:x-pack/platform/plugins/shared/search_inference_endpoints", + "@kbn/search-inference-endpoints-fixture-plugin": "link:x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture", "@kbn/search-navigation": "link:x-pack/solutions/search/plugins/search_navigation", "@kbn/search-notebooks": "link:x-pack/solutions/search/plugins/search_notebooks", "@kbn/search-playground": "link:x-pack/solutions/search/plugins/search_playground", diff --git a/src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/inference_test/stateful/classic.stateful.config.ts b/src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/inference_test/stateful/classic.stateful.config.ts new file mode 100644 index 0000000000000..9b5406674f761 --- /dev/null +++ b/src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/inference_test/stateful/classic.stateful.config.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", 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 { resolve } from 'path'; +import { REPO_ROOT } from '@kbn/repo-info'; +import type { ScoutServerConfig } from '../../../../../types'; +import { defaultConfig } from '../../default/stateful/base.config'; + +const pluginPath = `--plugin-path=${resolve( + REPO_ROOT, + 'x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture' +)}`; + +export const servers: ScoutServerConfig = { + ...defaultConfig, + kbnTestServer: { + ...defaultConfig.kbnTestServer, + serverArgs: [...defaultConfig.kbnTestServer.serverArgs, pluginPath], + }, +}; diff --git a/tsconfig.base.json b/tsconfig.base.json index 3265b461a9912..20618362dd4d2 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -2040,6 +2040,8 @@ "@kbn/search-index-documents/*": ["x-pack/platform/packages/shared/kbn-search-index-documents/*"], "@kbn/search-inference-endpoints": ["x-pack/platform/plugins/shared/search_inference_endpoints"], "@kbn/search-inference-endpoints/*": ["x-pack/platform/plugins/shared/search_inference_endpoints/*"], + "@kbn/search-inference-endpoints-fixture-plugin": ["x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture"], + "@kbn/search-inference-endpoints-fixture-plugin/*": ["x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/*"], "@kbn/search-navigation": ["x-pack/solutions/search/plugins/search_navigation"], "@kbn/search-navigation/*": ["x-pack/solutions/search/plugins/search_navigation/*"], "@kbn/search-notebooks": ["x-pack/solutions/search/plugins/search_notebooks"], diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts deleted file mode 100644 index 2036621ba6ed1..0000000000000 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/tests/feature_settings.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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 { tags } from '@kbn/scout'; -import { expect } from '@kbn/scout/ui'; -import { test } from '../fixtures'; -import { mockInferenceEndpoints } from '../fixtures/mock_data/inference_endpoints'; - -test.describe( - 'Feature Settings', - { tag: [...tags.stateful.classic, ...tags.serverless.search] }, - () => { - test.beforeEach(async ({ browserAuth, pageObjects }) => { - await browserAuth.loginAsPrivilegedUser(); - await pageObjects.featureSettings.goto(); - }); - - test('page header is visible', async ({ pageObjects }) => { - await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); - }); - - test('page loads with default model section and controls', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('header controls are present', async () => { - await expect(featureSettings.saveButton).toBeVisible(); - await expect(featureSettings.saveButton).toBeDisabled(); - await expect(featureSettings.apiDocumentationLink).toBeVisible(); - }); - - await test.step('default model section is visible', async () => { - await expect(featureSettings.defaultModelSection).toBeVisible(); - await expect(featureSettings.defaultModelComboBox).toBeVisible(); - await expect(featureSettings.disallowOtherModelsCheckbox).toBeVisible(); - }); - }); - - test('disallow other models hides feature sections', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('enable disallow other models', async () => { - await featureSettings.disallowOtherModelsCheckbox.click(); - }); - - await test.step('feature sections are hidden', async () => { - await expect(featureSettings.allFeatureSections).toHaveCount(0); - }); - - await test.step('disable disallow other models', async () => { - await featureSettings.disallowOtherModelsCheckbox.click(); - }); - - await test.step('feature sections reappear', async () => { - await expect(featureSettings.allFeatureSections).not.toHaveCount(0); - }); - }); - - test('feature sections render with sub-feature cards', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('at least one feature section is visible', async () => { - await expect(featureSettings.allFeatureSections).not.toHaveCount(0); - }); - - await test.step('sub-feature cards are present with endpoint rows', async () => { - await expect(featureSettings.allSubFeatureCards).not.toHaveCount(0); - await expect(featureSettings.allEndpointRows).not.toHaveCount(0); - }); - - await test.step('first endpoint row has a default badge', async () => { - await expect(featureSettings.firstEndpointRow).toContainText('Default'); - }); - }); - - test('add model popover opens with search', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('open add model popover on a sub-feature', async () => { - await featureSettings.firstAddModelButton.click(); - await expect(featureSettings.addModelSearch).toBeVisible(); - }); - }); - - test('reset to defaults modal cancel', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('click reset link opens confirmation modal', async () => { - await featureSettings.firstResetLink.click(); - await expect(featureSettings.resetDefaultsModal).toBeVisible(); - }); - - await test.step('cancel closes the modal without changes', async () => { - await featureSettings.resetDefaultsCancelButton.click(); - await expect(featureSettings.resetDefaultsModal).toBeHidden(); - await expect(featureSettings.saveButton).toBeDisabled(); - }); - }); - - test('copy to modal', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('click copy to opens modal', async () => { - await featureSettings.firstCopyToButton.click(); - await expect(featureSettings.copyToModalApply).toBeVisible(); - await expect(featureSettings.copyToModalApply).toBeDisabled(); - }); - - await test.step('cancel closes the modal', async () => { - await featureSettings.copyToModalCancel.click(); - await expect(featureSettings.copyToModalApply).toBeHidden(); - }); - }); - - test('add model popover search filters results', async ({ page, pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('mock inference endpoints with chat_completion models', async () => { - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); - await page.reload(); - await featureSettings.goto(); - }); - - await test.step('open popover and verify models are listed', async () => { - await featureSettings.firstAddModelButton.click(); - await expect(featureSettings.addModelSearch).toBeVisible(); - await expect(featureSettings.addModelOptions).not.toHaveCount(0); - }); - - await test.step('search filters the model list', async () => { - await featureSettings.addModelSearch.fill('anthropic'); - await expect(featureSettings.addModelOptions).toHaveCount(1); - await expect(featureSettings.addModelOptions).toContainText('anthropic'); - }); - }); - - test('selecting a model adds it to the assigned models list', async ({ page, pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('mock inference endpoints', async () => { - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); - await page.reload(); - await featureSettings.goto(); - }); - - await test.step('count existing endpoint rows', async () => { - await expect(featureSettings.allEndpointRows).not.toHaveCount(0); - }); - - const initialCount = await featureSettings.allEndpointRows.count(); - - await test.step('select a model from the popover', async () => { - await featureSettings.firstAddModelButton.click(); - await expect(featureSettings.addModelSearch).toBeVisible(); - // eslint-disable-next-line playwright/no-nth-methods -- selecting the first available model from the popover list - await featureSettings.addModelOptions.first().click(); - }); - - await test.step('endpoint row count increases', async () => { - await expect(featureSettings.allEndpointRows).toHaveCount(initialCount + 1); - }); - - await test.step('save button becomes enabled', async () => { - await expect(featureSettings.saveButton).toBeEnabled(); - }); - }); - } -); diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/index.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/index.ts similarity index 100% rename from x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/index.ts rename to x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/index.ts diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/mock_data/inference_endpoints.ts similarity index 84% rename from x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts rename to x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/mock_data/inference_endpoints.ts index 1fbde78620cf2..4c40f6d48c307 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/mock_data/inference_endpoints.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/mock_data/inference_endpoints.ts @@ -7,7 +7,7 @@ export const mockInferenceEndpoints = [ { - inference_id: '.anthropic-claude-3.7-sonnet-chat_completion-a3f1', + inference_id: '.mock-anthropic-claude-3.7-sonnet-chat_completion-a3f1', task_type: 'chat_completion', service: 'elastic', service_settings: { model_id: 'anthropic-claude-3.7-sonnet' }, @@ -17,7 +17,7 @@ export const mockInferenceEndpoints = [ }, }, { - inference_id: '.anthropic-claude-3.7-sonnet-completion-b7d2', + inference_id: '.mock-anthropic-claude-3.7-sonnet-completion-b7d2', task_type: 'completion', service: 'elastic', service_settings: { model_id: 'anthropic-claude-3.7-sonnet' }, @@ -27,7 +27,7 @@ export const mockInferenceEndpoints = [ }, }, { - inference_id: '.openai-gpt-4.1-chat_completion-c9e4', + inference_id: '.mock-openai-gpt-4.1-chat_completion-c9e4', task_type: 'chat_completion', service: 'elastic', service_settings: { model_id: 'openai-gpt-4.1' }, @@ -37,7 +37,7 @@ export const mockInferenceEndpoints = [ }, }, { - inference_id: '.openai-gpt-4.1-completion-d5f6', + inference_id: '.mock-openai-gpt-4.1-completion-d5f6', task_type: 'completion', service: 'elastic', service_settings: { model_id: 'openai-gpt-4.1' }, @@ -47,7 +47,7 @@ export const mockInferenceEndpoints = [ }, }, { - inference_id: '.google-gemini-2.5-pro-chat_completion-e2a8', + inference_id: '.mock-google-gemini-2.5-pro-chat_completion-e2a8', task_type: 'chat_completion', service: 'elastic', service_settings: { model_id: 'google-gemini-2.5-pro' }, @@ -57,7 +57,7 @@ export const mockInferenceEndpoints = [ }, }, { - inference_id: '.google-gemini-2.5-pro-completion-f1b3', + inference_id: '.mock-google-gemini-2.5-pro-completion-f1b3', task_type: 'completion', service: 'elastic', service_settings: { model_id: 'google-gemini-2.5-pro' }, diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts similarity index 80% rename from x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts rename to x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts index 7a4e95741f881..a68b17913534f 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts @@ -65,27 +65,45 @@ export class FeatureSettingsPage { return this.content.locator('[data-test-subj^="endpoint-row-"]'); } + public endpointRowsFor(featureId: string): Locator { + return this.subFeatureCard(featureId).locator('[data-test-subj^="endpoint-row-"]'); + } + public get firstEndpointRow(): Locator { // eslint-disable-next-line playwright/no-nth-methods -- selecting the first endpoint row to verify default badge presence return this.allEndpointRows.first(); } + // --- Sub-Feature Card by ID --- + + public subFeatureCard(featureId: string): Locator { + return this.page.testSubj.locator(`subFeatureCard-${featureId}`); + } + // --- Add Model Popover --- - public get firstAddModelButton(): Locator { - // eslint-disable-next-line playwright/no-nth-methods -- multiple sub-features have add-model buttons; picking the first to test the popover flow - return this.content.locator('[data-test-subj="add-model-button"]').first(); + public addModelButton(featureId: string): Locator { + return this.subFeatureCard(featureId).locator('[data-test-subj="add-model-button"]'); } public get addModelSearch(): Locator { return this.page.testSubj.locator('add-model-search'); } + // --- Add Model Popover Options --- + + public get addModelOptions(): Locator { + return this.page.testSubj.locator('add-model-selectable').getByRole('option'); + } + // --- Copy To Modal --- - public get firstCopyToButton(): Locator { - // eslint-disable-next-line playwright/no-nth-methods -- multiple sub-features have copy-to buttons; picking the first to test the modal flow - return this.content.locator('[data-test-subj^="copy-to-"]').first(); + public copyToButton(featureId: string): Locator { + return this.page.testSubj.locator(`copy-to-${featureId}`); + } + + public copyToModalCheckbox(featureId: string): Locator { + return this.page.locator(`#copy-target-${featureId}`); } public get copyToModalApply(): Locator { @@ -98,9 +116,8 @@ export class FeatureSettingsPage { // --- Reset Defaults Modal --- - public get firstResetLink(): Locator { - // eslint-disable-next-line playwright/no-nth-methods -- multiple sub-features have reset links; picking the first to test the modal flow - return this.content.locator('[data-test-subj^="reset-"]').first(); + public resetLink(parentName: string): Locator { + return this.page.testSubj.locator(`reset-${parentName}`); } public get resetDefaultsModal(): Locator { @@ -111,12 +128,6 @@ export class FeatureSettingsPage { return this.resetDefaultsModal.locator('[data-test-subj="confirmModalCancelButton"]'); } - // --- Add Model Popover Options --- - - public get addModelOptions(): Locator { - return this.page.testSubj.locator('add-model-selectable').getByRole('option'); - } - // --- Route Mocking --- public async mockInferenceEndpoints(endpoints: unknown[]) { diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/index.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/index.ts similarity index 100% rename from x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/fixtures/page_objects/index.ts rename to x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/index.ts diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/playwright.config.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/playwright.config.ts similarity index 100% rename from x-pack/platform/plugins/shared/search_inference_endpoints/test/scout/ui/playwright.config.ts rename to x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/playwright.config.ts diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts new file mode 100644 index 0000000000000..d3475d920e171 --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -0,0 +1,225 @@ +/* + * 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 { tags } from '@kbn/scout'; +import { expect } from '@kbn/scout/ui'; +import { test } from '../fixtures'; +import { mockInferenceEndpoints } from '../fixtures/mock_data/inference_endpoints'; + +test.describe('Feature Settings', { tag: [...tags.stateful.classic] }, () => { + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsPrivilegedUser(); + await pageObjects.featureSettings.goto(); + }); + + test('page header is visible', async ({ pageObjects }) => { + await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); + }); + + test('page loads with default model section and controls', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('header controls are present', async () => { + await expect(featureSettings.saveButton).toBeVisible(); + await expect(featureSettings.saveButton).toBeDisabled(); + await expect(featureSettings.apiDocumentationLink).toBeVisible(); + }); + + await test.step('default model section is visible', async () => { + await expect(featureSettings.defaultModelSection).toBeVisible(); + await expect(featureSettings.defaultModelComboBox).toBeVisible(); + await expect(featureSettings.disallowOtherModelsCheckbox).toBeVisible(); + }); + }); + + test('disallow other models hides feature sections', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('enable disallow other models', async () => { + await featureSettings.disallowOtherModelsCheckbox.click(); + }); + + await test.step('feature sections are hidden', async () => { + await expect(featureSettings.allFeatureSections).toHaveCount(0); + }); + + await test.step('disable disallow other models', async () => { + await featureSettings.disallowOtherModelsCheckbox.click(); + }); + + await test.step('feature sections reappear', async () => { + await expect(featureSettings.allFeatureSections).not.toHaveCount(0); + }); + }); + + test('feature sections render with sub-feature cards', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('at least one feature section is visible', async () => { + await expect(featureSettings.allFeatureSections).not.toHaveCount(0); + }); + + await test.step('sub-feature cards are present with endpoint rows', async () => { + await expect(featureSettings.allSubFeatureCards).not.toHaveCount(0); + await expect(featureSettings.allEndpointRows).not.toHaveCount(0); + }); + + await test.step('first endpoint row has a default badge', async () => { + await expect(featureSettings.firstEndpointRow).toContainText('Default'); + }); + }); + + test('fixture plugin registers Test Inference feature section', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('Test Inference section is visible', async () => { + await expect(featureSettings.content).toContainText('Test Inference'); + }); + + await test.step('Test Feature Alpha sub-feature is visible', async () => { + await expect(featureSettings.content).toContainText('Test Feature Alpha'); + }); + + await test.step('Test Feature Beta sub-feature is visible', async () => { + await expect(featureSettings.content).toContainText('Test Feature Beta'); + }); + }); + + test('add model popover opens with search on Alpha', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('open add model popover on Alpha', async () => { + await featureSettings.addModelButton('test_feature_alpha').click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + }); + }); + + test('add model popover search filters results on Alpha', async ({ page, pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('mock inference endpoints', async () => { + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await page.reload(); + await featureSettings.goto(); + }); + + await test.step('open popover on Alpha and verify models are listed', async () => { + await featureSettings.addModelButton('test_feature_alpha').click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + await expect(featureSettings.addModelOptions).not.toHaveCount(0); + }); + + await test.step('search filters the model list', async () => { + const countBeforeSearch = await featureSettings.addModelOptions.count(); + await featureSettings.addModelSearch.fill('anthropic'); + const countAfterSearch = await featureSettings.addModelOptions.count(); + expect(countAfterSearch).toBeLessThan(countBeforeSearch); + expect(countAfterSearch).toBeGreaterThan(0); + }); + }); + + test('selecting a model on Alpha adds it to the assigned models list', async ({ + page, + pageObjects, + }) => { + const { featureSettings } = pageObjects; + + await test.step('mock inference endpoints', async () => { + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await page.reload(); + await featureSettings.goto(); + }); + + const initialCount = await featureSettings.allEndpointRows.count(); + + await test.step('select a model from Alpha popover', async () => { + await featureSettings.addModelButton('test_feature_alpha').click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + // eslint-disable-next-line playwright/no-nth-methods -- selecting the first available model from the popover list + await featureSettings.addModelOptions.first().click(); + }); + + await test.step('endpoint row count increases', async () => { + await expect(featureSettings.allEndpointRows).toHaveCount(initialCount + 1); + }); + + await test.step('save button becomes enabled', async () => { + await expect(featureSettings.saveButton).toBeEnabled(); + }); + }); + + test('reset to defaults modal cancel on Test Inference', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('click reset link opens confirmation modal', async () => { + await featureSettings.resetLink('Test Inference').click(); + await expect(featureSettings.resetDefaultsModal).toBeVisible(); + }); + + await test.step('cancel closes the modal without changes', async () => { + await featureSettings.resetDefaultsCancelButton.click(); + await expect(featureSettings.resetDefaultsModal).toBeHidden(); + await expect(featureSettings.saveButton).toBeDisabled(); + }); + }); + + test('copy to modal cancel on Alpha', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('click copy to on Alpha opens modal', async () => { + await featureSettings.copyToButton('test_feature_alpha').click(); + await expect(featureSettings.copyToModalApply).toBeVisible(); + await expect(featureSettings.copyToModalApply).toBeDisabled(); + }); + + await test.step('cancel closes the modal', async () => { + await featureSettings.copyToModalCancel.click(); + await expect(featureSettings.copyToModalApply).toBeHidden(); + }); + }); + + test('copy to from Alpha to Beta updates Beta endpoint list', async ({ page, pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('mock inference endpoints', async () => { + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await page.reload(); + await featureSettings.goto(); + }); + + const betaCard = featureSettings.subFeatureCard('test_feature_beta'); + + await test.step('Beta contains its original endpoint before copy', async () => { + await expect(betaCard).toContainText('openai'); + await expect(betaCard).not.toContainText('anthropic'); + }); + + await test.step('open copy-to modal from Alpha', async () => { + await featureSettings.copyToButton('test_feature_alpha').click(); + await expect(featureSettings.copyToModalApply).toBeVisible(); + await expect(featureSettings.copyToModalApply).toBeDisabled(); + }); + + await test.step('select Beta as target', async () => { + await featureSettings.copyToModalCheckbox('test_feature_beta').click(); + }); + + await test.step('apply copies Alpha models to Beta', async () => { + await expect(featureSettings.copyToModalApply).toBeEnabled(); + await featureSettings.copyToModalApply.click(); + await expect(featureSettings.copyToModalApply).toBeHidden(); + }); + + await test.step('Beta now contains Alpha endpoint after copy', async () => { + await expect(betaCard).toContainText('anthropic'); + }); + + await test.step('save button becomes enabled after copy', async () => { + await expect(featureSettings.saveButton).toBeEnabled(); + }); + }); +}); diff --git a/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/kibana.jsonc b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/kibana.jsonc new file mode 100644 index 0000000000000..238eae127d3fc --- /dev/null +++ b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/kibana.jsonc @@ -0,0 +1,15 @@ +{ + "type": "plugin", + "id": "@kbn/search-inference-endpoints-fixture-plugin", + "owner": "@elastic/search-kibana", + "group": "platform", + "visibility": "private", + "plugin": { + "id": "searchInferenceEndpointsFixture", + "server": true, + "browser": false, + "requiredPlugins": [ + "searchInferenceEndpoints" + ] + } +} diff --git a/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/moon.yml b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/moon.yml new file mode 100644 index 0000000000000..d0f2c8ac0ed60 --- /dev/null +++ b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/moon.yml @@ -0,0 +1,32 @@ +# This file is generated by the @kbn/moon package. Any manual edits will be erased! +# To extend this, write your extensions/overrides to 'moon.extend.yml' +# then regenerate this file with: 'node scripts/regenerate_moon_projects.js --update --filter @kbn/search-inference-endpoints-fixture-plugin' + +$schema: https://moonrepo.dev/schemas/project.json +id: '@kbn/search-inference-endpoints-fixture-plugin' +layer: unknown +owners: + defaultOwner: '@elastic/search-kibana' +toolchains: + default: node +language: typescript +project: + title: '@kbn/search-inference-endpoints-fixture-plugin' + description: Moon project for @kbn/search-inference-endpoints-fixture-plugin + channel: '' + owner: '@elastic/search-kibana' + sourceRoot: x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture +dependsOn: + - '@kbn/core' + - '@kbn/core-plugins-server' + - '@kbn/search-inference-endpoints' +tags: + - plugin + - prod + - group-platform + - private +fileGroups: + src: + - server/**/* + - '!target/**/*' +tasks: {} diff --git a/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/package.json b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/package.json new file mode 100644 index 0000000000000..ff86ef3366be3 --- /dev/null +++ b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/package.json @@ -0,0 +1,13 @@ +{ + "name": "@kbn/search-inference-endpoints-fixture-plugin", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "scripts": { + "kbn": "node ../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../../node_modules/.bin/tsc" + }, + "license": "Elastic License 2.0" +} diff --git a/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/server/index.ts b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/server/index.ts new file mode 100644 index 0000000000000..a2d520804b20b --- /dev/null +++ b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/server/index.ts @@ -0,0 +1,13 @@ +/* + * 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 type { PluginInitializerContext } from '@kbn/core-plugins-server'; +import { SearchInferenceEndpointsFixturePlugin } from './plugin'; + +export const plugin = async (context: PluginInitializerContext<{}>) => { + return new SearchInferenceEndpointsFixturePlugin(context); +}; diff --git a/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/server/plugin.ts b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/server/plugin.ts new file mode 100644 index 0000000000000..42c738368ab3d --- /dev/null +++ b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/server/plugin.ts @@ -0,0 +1,52 @@ +/* + * 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 type { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import type { SearchInferenceEndpointsPluginSetup } from '@kbn/search-inference-endpoints/server'; + +interface SetupDeps { + searchInferenceEndpoints: SearchInferenceEndpointsPluginSetup; +} + +export class SearchInferenceEndpointsFixturePlugin implements Plugin { + constructor(context: PluginInitializerContext) {} + + setup(core: CoreSetup, { searchInferenceEndpoints }: SetupDeps) { + const { register } = searchInferenceEndpoints.features; + + register({ + featureId: 'test_inference_parent', + featureName: 'Test Inference', + featureDescription: 'Test parent feature for Scout integration tests', + taskType: 'chat_completion', + recommendedEndpoints: [ + '.anthropic-claude-3.7-sonnet-chat_completion', + '.openai-gpt-4.1-chat_completion', + ], + }); + + register({ + featureId: 'test_feature_alpha', + parentFeatureId: 'test_inference_parent', + featureName: 'Test Feature Alpha', + featureDescription: 'First test child feature for verifying Add Model and Copy To flows', + taskType: 'chat_completion', + recommendedEndpoints: ['.anthropic-claude-3.7-sonnet-chat_completion'], + }); + + register({ + featureId: 'test_feature_beta', + parentFeatureId: 'test_inference_parent', + featureName: 'Test Feature Beta', + featureDescription: 'Second test child feature for verifying Copy To target behavior', + taskType: 'chat_completion', + recommendedEndpoints: ['.openai-gpt-4.1-chat_completion'], + }); + } + + start() {} +} diff --git a/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/tsconfig.json b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/tsconfig.json new file mode 100644 index 0000000000000..70af213e2217a --- /dev/null +++ b/x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "@kbn/tsconfig-base/tsconfig.json", + "compilerOptions": { + "outDir": "target/types", + "isolatedModules": true + }, + "include": [ + "server/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/core-plugins-server", + "@kbn/search-inference-endpoints" + ], + "exclude": [ + "target/**/*" + ] +} diff --git a/yarn.lock b/yarn.lock index b7ec84ef2ea46..789774a488f28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2455,7 +2455,7 @@ resolved "https://registry.yarnpkg.com/@elastic/filesaver/-/filesaver-1.1.2.tgz#1998ffb3cd89c9da4ec12a7793bfcae10e30c77a" integrity sha512-YZbSufYFBhAj+S2cJgiKALoxIJevqXN2MSr6Yqr42rJdaPuM31cj6pUDwflkql1oDjupqD9la+MfxPFjXI1JFQ== -"@elastic/kibana-d3-color@npm:@elastic/kibana-d3-color@2.0.1": +"@elastic/kibana-d3-color@npm:@elastic/kibana-d3-color@2.0.1", "d3-color@1 - 2", "d3-color@npm:@elastic/kibana-d3-color@2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@elastic/kibana-d3-color/-/kibana-d3-color-2.0.1.tgz#f83b9c2fea09273a918659de04d5e8098c82f65c" integrity sha512-YZ8hV2bWNyYi833Yj3UWczmTxdHzmo/Xc2IVkNXr/ZqtkrTDlTLysCyJm7SfAt9iBy6EVRGWTn8cPz8QOY6Ixw== @@ -8619,6 +8619,10 @@ version "0.0.0" uid "" +"@kbn/search-inference-endpoints-fixture-plugin@link:x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture": + version "0.0.0" + uid "" + "@kbn/search-inference-endpoints@link:x-pack/platform/plugins/shared/search_inference_endpoints": version "0.0.0" uid "" @@ -12132,7 +12136,7 @@ resolved "https://registry.yarnpkg.com/@readme/openapi-schemas/-/openapi-schemas-3.1.0.tgz#5ff4b704af6a8b108f9d577fd87cf73e9e7b3178" integrity sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw== -"@redocly/ajv@^8.11.2", "@redocly/ajv@^8.18.0": +"@redocly/ajv@^8.11.2", "@redocly/ajv@^8.18.0", "ajv@npm:@redocly/ajv@8.18.0": version "8.18.0" resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.18.0.tgz#e6c7ba549111838baa950bc31acbc84b06f0239f" integrity sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA== @@ -16150,16 +16154,6 @@ ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -"ajv@npm:@redocly/ajv@8.18.0": - version "8.18.0" - resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.18.0.tgz#e6c7ba549111838baa950bc31acbc84b06f0239f" - integrity sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA== - dependencies: - fast-deep-equal "^3.1.3" - fast-uri "^3.0.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - anser@^2.1.1: version "2.3.2" resolved "https://registry.yarnpkg.com/anser/-/anser-2.3.2.tgz#e2da9d10759a4243a5819595f4f46ec369970c5b" @@ -19267,11 +19261,6 @@ d3-collection@^1.0.7: resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== -"d3-color@1 - 2", "d3-color@npm:@elastic/kibana-d3-color@2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@elastic/kibana-d3-color/-/kibana-d3-color-2.0.1.tgz#f83b9c2fea09273a918659de04d5e8098c82f65c" - integrity sha512-YZ8hV2bWNyYi833Yj3UWczmTxdHzmo/Xc2IVkNXr/ZqtkrTDlTLysCyJm7SfAt9iBy6EVRGWTn8cPz8QOY6Ixw== - "d3-color@1 - 3", d3-color@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" @@ -32813,7 +32802,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -32831,15 +32820,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -32940,14 +32920,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -35739,7 +35712,7 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -35765,15 +35738,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 0b008fc9860bdf27a31f6d64c23500b566551ac9 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 12:48:32 -0600 Subject: [PATCH 19/34] Refactor code --- .../serverless/search.serverless.config.ts | 26 ++ .../ui/tests/feature_settings.spec.ts | 334 +++++++++--------- 2 files changed, 195 insertions(+), 165 deletions(-) create mode 100644 src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/inference_test/serverless/search.serverless.config.ts diff --git a/src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/inference_test/serverless/search.serverless.config.ts b/src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/inference_test/serverless/search.serverless.config.ts new file mode 100644 index 0000000000000..40875a7addb72 --- /dev/null +++ b/src/platform/packages/shared/kbn-scout/src/servers/configs/config_sets/inference_test/serverless/search.serverless.config.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", 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 { resolve } from 'path'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { servers as defaultConfig } from '../../default/serverless/search.serverless.config'; +import type { ScoutServerConfig } from '../../../../../types'; + +const pluginPath = `--plugin-path=${resolve( + REPO_ROOT, + 'x-pack/platform/test/search_inference_endpoints/plugins/search_inference_endpoints_fixture' +)}`; + +export const servers: ScoutServerConfig = { + ...defaultConfig, + kbnTestServer: { + ...defaultConfig.kbnTestServer, + serverArgs: [...defaultConfig.kbnTestServer.serverArgs, pluginPath], + }, +}; diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index d3475d920e171..2f6614f496729 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -10,216 +10,220 @@ import { expect } from '@kbn/scout/ui'; import { test } from '../fixtures'; import { mockInferenceEndpoints } from '../fixtures/mock_data/inference_endpoints'; -test.describe('Feature Settings', { tag: [...tags.stateful.classic] }, () => { - test.beforeEach(async ({ browserAuth, pageObjects }) => { - await browserAuth.loginAsPrivilegedUser(); - await pageObjects.featureSettings.goto(); - }); - - test('page header is visible', async ({ pageObjects }) => { - await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); - }); - - test('page loads with default model section and controls', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('header controls are present', async () => { - await expect(featureSettings.saveButton).toBeVisible(); - await expect(featureSettings.saveButton).toBeDisabled(); - await expect(featureSettings.apiDocumentationLink).toBeVisible(); +test.describe( + 'Feature Settings', + { tag: [...tags.stateful.classic, ...tags.serverless.search] }, + () => { + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsPrivilegedUser(); + await pageObjects.featureSettings.goto(); }); - await test.step('default model section is visible', async () => { - await expect(featureSettings.defaultModelSection).toBeVisible(); - await expect(featureSettings.defaultModelComboBox).toBeVisible(); - await expect(featureSettings.disallowOtherModelsCheckbox).toBeVisible(); + test('page header is visible', async ({ pageObjects }) => { + await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); }); - }); - test('disallow other models hides feature sections', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + test('page loads with default model section and controls', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('enable disallow other models', async () => { - await featureSettings.disallowOtherModelsCheckbox.click(); - }); + await test.step('header controls are present', async () => { + await expect(featureSettings.saveButton).toBeVisible(); + await expect(featureSettings.saveButton).toBeDisabled(); + await expect(featureSettings.apiDocumentationLink).toBeVisible(); + }); - await test.step('feature sections are hidden', async () => { - await expect(featureSettings.allFeatureSections).toHaveCount(0); + await test.step('default model section is visible', async () => { + await expect(featureSettings.defaultModelSection).toBeVisible(); + await expect(featureSettings.defaultModelComboBox).toBeVisible(); + await expect(featureSettings.disallowOtherModelsCheckbox).toBeVisible(); + }); }); - await test.step('disable disallow other models', async () => { - await featureSettings.disallowOtherModelsCheckbox.click(); - }); + test('disallow other models hides feature sections', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('feature sections reappear', async () => { - await expect(featureSettings.allFeatureSections).not.toHaveCount(0); - }); - }); + await test.step('enable disallow other models', async () => { + await featureSettings.disallowOtherModelsCheckbox.click(); + }); - test('feature sections render with sub-feature cards', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + await test.step('feature sections are hidden', async () => { + await expect(featureSettings.allFeatureSections).toHaveCount(0); + }); - await test.step('at least one feature section is visible', async () => { - await expect(featureSettings.allFeatureSections).not.toHaveCount(0); - }); + await test.step('disable disallow other models', async () => { + await featureSettings.disallowOtherModelsCheckbox.click(); + }); - await test.step('sub-feature cards are present with endpoint rows', async () => { - await expect(featureSettings.allSubFeatureCards).not.toHaveCount(0); - await expect(featureSettings.allEndpointRows).not.toHaveCount(0); + await test.step('feature sections reappear', async () => { + await expect(featureSettings.allFeatureSections).not.toHaveCount(0); + }); }); - await test.step('first endpoint row has a default badge', async () => { - await expect(featureSettings.firstEndpointRow).toContainText('Default'); - }); - }); + test('feature sections render with sub-feature cards', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - test('fixture plugin registers Test Inference feature section', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + await test.step('at least one feature section is visible', async () => { + await expect(featureSettings.allFeatureSections).not.toHaveCount(0); + }); - await test.step('Test Inference section is visible', async () => { - await expect(featureSettings.content).toContainText('Test Inference'); - }); + await test.step('sub-feature cards are present with endpoint rows', async () => { + await expect(featureSettings.allSubFeatureCards).not.toHaveCount(0); + await expect(featureSettings.allEndpointRows).not.toHaveCount(0); + }); - await test.step('Test Feature Alpha sub-feature is visible', async () => { - await expect(featureSettings.content).toContainText('Test Feature Alpha'); + await test.step('first endpoint row has a default badge', async () => { + await expect(featureSettings.firstEndpointRow).toContainText('Default'); + }); }); - await test.step('Test Feature Beta sub-feature is visible', async () => { - await expect(featureSettings.content).toContainText('Test Feature Beta'); - }); - }); - - test('add model popover opens with search on Alpha', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + test('fixture plugin registers Test Inference feature section', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('open add model popover on Alpha', async () => { - await featureSettings.addModelButton('test_feature_alpha').click(); - await expect(featureSettings.addModelSearch).toBeVisible(); - }); - }); + await test.step('Test Inference section is visible', async () => { + await expect(featureSettings.content).toContainText('Test Inference'); + }); - test('add model popover search filters results on Alpha', async ({ page, pageObjects }) => { - const { featureSettings } = pageObjects; + await test.step('Test Feature Alpha sub-feature is visible', async () => { + await expect(featureSettings.content).toContainText('Test Feature Alpha'); + }); - await test.step('mock inference endpoints', async () => { - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); - await page.reload(); - await featureSettings.goto(); + await test.step('Test Feature Beta sub-feature is visible', async () => { + await expect(featureSettings.content).toContainText('Test Feature Beta'); + }); }); - await test.step('open popover on Alpha and verify models are listed', async () => { - await featureSettings.addModelButton('test_feature_alpha').click(); - await expect(featureSettings.addModelSearch).toBeVisible(); - await expect(featureSettings.addModelOptions).not.toHaveCount(0); - }); + test('add model popover opens with search on Alpha', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('search filters the model list', async () => { - const countBeforeSearch = await featureSettings.addModelOptions.count(); - await featureSettings.addModelSearch.fill('anthropic'); - const countAfterSearch = await featureSettings.addModelOptions.count(); - expect(countAfterSearch).toBeLessThan(countBeforeSearch); - expect(countAfterSearch).toBeGreaterThan(0); - }); - }); - - test('selecting a model on Alpha adds it to the assigned models list', async ({ - page, - pageObjects, - }) => { - const { featureSettings } = pageObjects; - - await test.step('mock inference endpoints', async () => { - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); - await page.reload(); - await featureSettings.goto(); + await test.step('open add model popover on Alpha', async () => { + await featureSettings.addModelButton('test_feature_alpha').click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + }); }); - const initialCount = await featureSettings.allEndpointRows.count(); + test('add model popover search filters results on Alpha', async ({ page, pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('select a model from Alpha popover', async () => { - await featureSettings.addModelButton('test_feature_alpha').click(); - await expect(featureSettings.addModelSearch).toBeVisible(); - // eslint-disable-next-line playwright/no-nth-methods -- selecting the first available model from the popover list - await featureSettings.addModelOptions.first().click(); - }); + await test.step('mock inference endpoints', async () => { + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await page.reload(); + await featureSettings.goto(); + }); - await test.step('endpoint row count increases', async () => { - await expect(featureSettings.allEndpointRows).toHaveCount(initialCount + 1); - }); + await test.step('open popover on Alpha and verify models are listed', async () => { + await featureSettings.addModelButton('test_feature_alpha').click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + await expect(featureSettings.addModelOptions).not.toHaveCount(0); + }); - await test.step('save button becomes enabled', async () => { - await expect(featureSettings.saveButton).toBeEnabled(); + await test.step('search filters the model list', async () => { + const countBeforeSearch = await featureSettings.addModelOptions.count(); + await featureSettings.addModelSearch.fill('anthropic'); + const countAfterSearch = await featureSettings.addModelOptions.count(); + expect(countAfterSearch).toBeLessThan(countBeforeSearch); + expect(countAfterSearch).toBeGreaterThan(0); + }); }); - }); - test('reset to defaults modal cancel on Test Inference', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + test('selecting a model on Alpha adds it to the assigned models list', async ({ + page, + pageObjects, + }) => { + const { featureSettings } = pageObjects; - await test.step('click reset link opens confirmation modal', async () => { - await featureSettings.resetLink('Test Inference').click(); - await expect(featureSettings.resetDefaultsModal).toBeVisible(); - }); + await test.step('mock inference endpoints', async () => { + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await page.reload(); + await featureSettings.goto(); + }); - await test.step('cancel closes the modal without changes', async () => { - await featureSettings.resetDefaultsCancelButton.click(); - await expect(featureSettings.resetDefaultsModal).toBeHidden(); - await expect(featureSettings.saveButton).toBeDisabled(); - }); - }); + const initialCount = await featureSettings.allEndpointRows.count(); - test('copy to modal cancel on Alpha', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; + await test.step('select a model from Alpha popover', async () => { + await featureSettings.addModelButton('test_feature_alpha').click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + // eslint-disable-next-line playwright/no-nth-methods -- selecting the first available model from the popover list + await featureSettings.addModelOptions.first().click(); + }); - await test.step('click copy to on Alpha opens modal', async () => { - await featureSettings.copyToButton('test_feature_alpha').click(); - await expect(featureSettings.copyToModalApply).toBeVisible(); - await expect(featureSettings.copyToModalApply).toBeDisabled(); - }); + await test.step('endpoint row count increases', async () => { + await expect(featureSettings.allEndpointRows).toHaveCount(initialCount + 1); + }); - await test.step('cancel closes the modal', async () => { - await featureSettings.copyToModalCancel.click(); - await expect(featureSettings.copyToModalApply).toBeHidden(); + await test.step('save button becomes enabled', async () => { + await expect(featureSettings.saveButton).toBeEnabled(); + }); }); - }); - test('copy to from Alpha to Beta updates Beta endpoint list', async ({ page, pageObjects }) => { - const { featureSettings } = pageObjects; + test('reset to defaults modal cancel on Test Inference', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('mock inference endpoints', async () => { - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); - await page.reload(); - await featureSettings.goto(); - }); + await test.step('click reset link opens confirmation modal', async () => { + await featureSettings.resetLink('Test Inference').click(); + await expect(featureSettings.resetDefaultsModal).toBeVisible(); + }); - const betaCard = featureSettings.subFeatureCard('test_feature_beta'); - - await test.step('Beta contains its original endpoint before copy', async () => { - await expect(betaCard).toContainText('openai'); - await expect(betaCard).not.toContainText('anthropic'); + await test.step('cancel closes the modal without changes', async () => { + await featureSettings.resetDefaultsCancelButton.click(); + await expect(featureSettings.resetDefaultsModal).toBeHidden(); + await expect(featureSettings.saveButton).toBeDisabled(); + }); }); - await test.step('open copy-to modal from Alpha', async () => { - await featureSettings.copyToButton('test_feature_alpha').click(); - await expect(featureSettings.copyToModalApply).toBeVisible(); - await expect(featureSettings.copyToModalApply).toBeDisabled(); - }); + test('copy to modal cancel on Alpha', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; - await test.step('select Beta as target', async () => { - await featureSettings.copyToModalCheckbox('test_feature_beta').click(); - }); + await test.step('click copy to on Alpha opens modal', async () => { + await featureSettings.copyToButton('test_feature_alpha').click(); + await expect(featureSettings.copyToModalApply).toBeVisible(); + await expect(featureSettings.copyToModalApply).toBeDisabled(); + }); - await test.step('apply copies Alpha models to Beta', async () => { - await expect(featureSettings.copyToModalApply).toBeEnabled(); - await featureSettings.copyToModalApply.click(); - await expect(featureSettings.copyToModalApply).toBeHidden(); - }); + await test.step('cancel closes the modal', async () => { + await featureSettings.copyToModalCancel.click(); + await expect(featureSettings.copyToModalApply).toBeHidden(); + }); + }); - await test.step('Beta now contains Alpha endpoint after copy', async () => { - await expect(betaCard).toContainText('anthropic'); - }); + test('copy to from Alpha to Beta updates Beta endpoint list', async ({ page, pageObjects }) => { + const { featureSettings } = pageObjects; + + await test.step('mock inference endpoints', async () => { + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await page.reload(); + await featureSettings.goto(); + }); + + const betaCard = featureSettings.subFeatureCard('test_feature_beta'); + + await test.step('Beta contains its original endpoint before copy', async () => { + await expect(betaCard).toContainText('openai'); + await expect(betaCard).not.toContainText('anthropic'); + }); + + await test.step('open copy-to modal from Alpha', async () => { + await featureSettings.copyToButton('test_feature_alpha').click(); + await expect(featureSettings.copyToModalApply).toBeVisible(); + await expect(featureSettings.copyToModalApply).toBeDisabled(); + }); + + await test.step('select Beta as target', async () => { + await featureSettings.copyToModalCheckbox('test_feature_beta').click(); + }); + + await test.step('apply copies Alpha models to Beta', async () => { + await expect(featureSettings.copyToModalApply).toBeEnabled(); + await featureSettings.copyToModalApply.click(); + await expect(featureSettings.copyToModalApply).toBeHidden(); + }); + + await test.step('Beta now contains Alpha endpoint after copy', async () => { + await expect(betaCard).toContainText('anthropic'); + }); - await test.step('save button becomes enabled after copy', async () => { - await expect(featureSettings.saveButton).toBeEnabled(); + await test.step('save button becomes enabled after copy', async () => { + await expect(featureSettings.saveButton).toBeEnabled(); + }); }); - }); -}); + } +); From b9c0268d4db8d66ba913e87f8b3ef50dadbb8d9d Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 13:12:50 -0600 Subject: [PATCH 20/34] Refactor code --- yarn.lock | 50 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2759e90b38009..3a801394292c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2455,7 +2455,7 @@ resolved "https://registry.yarnpkg.com/@elastic/filesaver/-/filesaver-1.1.2.tgz#1998ffb3cd89c9da4ec12a7793bfcae10e30c77a" integrity sha512-YZbSufYFBhAj+S2cJgiKALoxIJevqXN2MSr6Yqr42rJdaPuM31cj6pUDwflkql1oDjupqD9la+MfxPFjXI1JFQ== -"@elastic/kibana-d3-color@npm:@elastic/kibana-d3-color@2.0.1", "d3-color@1 - 2", "d3-color@npm:@elastic/kibana-d3-color@2.0.1": +"@elastic/kibana-d3-color@npm:@elastic/kibana-d3-color@2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@elastic/kibana-d3-color/-/kibana-d3-color-2.0.1.tgz#f83b9c2fea09273a918659de04d5e8098c82f65c" integrity sha512-YZ8hV2bWNyYi833Yj3UWczmTxdHzmo/Xc2IVkNXr/ZqtkrTDlTLysCyJm7SfAt9iBy6EVRGWTn8cPz8QOY6Ixw== @@ -12140,7 +12140,7 @@ resolved "https://registry.yarnpkg.com/@readme/openapi-schemas/-/openapi-schemas-3.1.0.tgz#5ff4b704af6a8b108f9d577fd87cf73e9e7b3178" integrity sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw== -"@redocly/ajv@^8.11.2", "@redocly/ajv@^8.18.0", "ajv@npm:@redocly/ajv@8.18.0": +"@redocly/ajv@^8.11.2", "@redocly/ajv@^8.18.0": version "8.18.0" resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.18.0.tgz#e6c7ba549111838baa950bc31acbc84b06f0239f" integrity sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA== @@ -16158,6 +16158,16 @@ ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +"ajv@npm:@redocly/ajv@8.18.0": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.18.0.tgz#e6c7ba549111838baa950bc31acbc84b06f0239f" + integrity sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + anser@^2.1.1: version "2.3.2" resolved "https://registry.yarnpkg.com/anser/-/anser-2.3.2.tgz#e2da9d10759a4243a5819595f4f46ec369970c5b" @@ -19265,6 +19275,11 @@ d3-collection@^1.0.7: resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== +"d3-color@1 - 2", "d3-color@npm:@elastic/kibana-d3-color@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@elastic/kibana-d3-color/-/kibana-d3-color-2.0.1.tgz#f83b9c2fea09273a918659de04d5e8098c82f65c" + integrity sha512-YZ8hV2bWNyYi833Yj3UWczmTxdHzmo/Xc2IVkNXr/ZqtkrTDlTLysCyJm7SfAt9iBy6EVRGWTn8cPz8QOY6Ixw== + "d3-color@1 - 3", d3-color@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" @@ -32806,7 +32821,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -32824,6 +32839,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -32924,7 +32948,14 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -35716,7 +35747,7 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -35742,6 +35773,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 14f23ed67c3458331f2d14d451eab099e9c42ed1 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 13:16:06 -0600 Subject: [PATCH 21/34] Refactor code --- .../platform/plugins/shared/search_inference_endpoints/moon.yml | 2 +- .../plugins/shared/search_inference_endpoints/tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml b/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml index 6adf0379722a3..a841701c1e62f 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/moon.yml @@ -66,7 +66,7 @@ fileGroups: - common/**/* - public/**/* - server/**/* - - test/scout/**/* + - test/scout_inference_test/**/* - '!target/**/*' jest-config: - jest.config.js diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json b/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json index 7a881be72c679..e9a870337308a 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/tsconfig.json @@ -8,7 +8,7 @@ "common/**/*", "public/**/*", "server/**/*", - "test/scout/**/*" + "test/scout_inference_test/**/*" ], "kbn_references": [ "@kbn/config-schema", From 70f7ee658bdc506c5ec6bd4a329f12626344bbf8 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 13:46:20 -0600 Subject: [PATCH 22/34] Refactor code --- .../page_objects/feature_settings_page.ts | 12 - .../ui/tests/feature_settings.spec.ts | 244 ++++++++++-------- 2 files changed, 132 insertions(+), 124 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts index a68b17913534f..ffd4f38ecd69f 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts @@ -127,16 +127,4 @@ export class FeatureSettingsPage { public get resetDefaultsCancelButton(): Locator { return this.resetDefaultsModal.locator('[data-test-subj="confirmModalCancelButton"]'); } - - // --- Route Mocking --- - - public async mockInferenceEndpoints(endpoints: unknown[]) { - await this.page.route('**/internal/inference_endpoints/endpoints', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ inference_endpoints: endpoints }), - }); - }); - } } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index 2f6614f496729..63ef872d1c60c 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -10,6 +10,18 @@ import { expect } from '@kbn/scout/ui'; import { test } from '../fixtures'; import { mockInferenceEndpoints } from '../fixtures/mock_data/inference_endpoints'; +const MOCK_ROUTE = '**/internal/inference_endpoints/endpoints'; + +async function setupMockEndpoints(page: Parameters[2]>[0]['page']) { + await page.route(MOCK_ROUTE, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ inference_endpoints: mockInferenceEndpoints }), + }); + }); +} + test.describe( 'Feature Settings', { tag: [...tags.stateful.classic, ...tags.serverless.search] }, @@ -19,27 +31,29 @@ test.describe( await pageObjects.featureSettings.goto(); }); - test('page header is visible', async ({ pageObjects }) => { + test('displays page header', async ({ pageObjects }) => { await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); }); - test('page loads with default model section and controls', async ({ pageObjects }) => { + test('renders default model section and controls', async ({ pageObjects }) => { const { featureSettings } = pageObjects; - await test.step('header controls are present', async () => { + await test.step('verify header controls', async () => { await expect(featureSettings.saveButton).toBeVisible(); await expect(featureSettings.saveButton).toBeDisabled(); await expect(featureSettings.apiDocumentationLink).toBeVisible(); }); - await test.step('default model section is visible', async () => { + await test.step('verify default model section', async () => { await expect(featureSettings.defaultModelSection).toBeVisible(); await expect(featureSettings.defaultModelComboBox).toBeVisible(); await expect(featureSettings.disallowOtherModelsCheckbox).toBeVisible(); }); }); - test('disallow other models hides feature sections', async ({ pageObjects }) => { + test('toggling disallow other models hides and restores feature sections', async ({ + pageObjects, + }) => { const { featureSettings } = pageObjects; await test.step('enable disallow other models', async () => { @@ -59,7 +73,9 @@ test.describe( }); }); - test('feature sections render with sub-feature cards', async ({ pageObjects }) => { + test('renders feature sections with sub-feature cards and endpoint rows', async ({ + pageObjects, + }) => { const { featureSettings } = pageObjects; await test.step('at least one feature section is visible', async () => { @@ -76,104 +92,46 @@ test.describe( }); }); - test('fixture plugin registers Test Inference feature section', async ({ pageObjects }) => { + test('renders fixture sub-feature cards', async ({ pageObjects }) => { const { featureSettings } = pageObjects; - await test.step('Test Inference section is visible', async () => { - await expect(featureSettings.content).toContainText('Test Inference'); - }); - - await test.step('Test Feature Alpha sub-feature is visible', async () => { - await expect(featureSettings.content).toContainText('Test Feature Alpha'); + await test.step('first fixture sub-feature card is visible', async () => { + await expect(featureSettings.subFeatureCard('test_feature_alpha')).toBeVisible(); }); - await test.step('Test Feature Beta sub-feature is visible', async () => { - await expect(featureSettings.content).toContainText('Test Feature Beta'); + await test.step('second fixture sub-feature card is visible', async () => { + await expect(featureSettings.subFeatureCard('test_feature_beta')).toBeVisible(); }); }); - test('add model popover opens with search on Alpha', async ({ pageObjects }) => { + test('opens add model popover with search input', async ({ pageObjects }) => { const { featureSettings } = pageObjects; - await test.step('open add model popover on Alpha', async () => { - await featureSettings.addModelButton('test_feature_alpha').click(); - await expect(featureSettings.addModelSearch).toBeVisible(); - }); - }); - - test('add model popover search filters results on Alpha', async ({ page, pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('mock inference endpoints', async () => { - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); - await page.reload(); - await featureSettings.goto(); - }); - - await test.step('open popover on Alpha and verify models are listed', async () => { + await test.step('click add model button and verify search is visible', async () => { await featureSettings.addModelButton('test_feature_alpha').click(); await expect(featureSettings.addModelSearch).toBeVisible(); - await expect(featureSettings.addModelOptions).not.toHaveCount(0); - }); - - await test.step('search filters the model list', async () => { - const countBeforeSearch = await featureSettings.addModelOptions.count(); - await featureSettings.addModelSearch.fill('anthropic'); - const countAfterSearch = await featureSettings.addModelOptions.count(); - expect(countAfterSearch).toBeLessThan(countBeforeSearch); - expect(countAfterSearch).toBeGreaterThan(0); }); }); - test('selecting a model on Alpha adds it to the assigned models list', async ({ - page, - pageObjects, - }) => { + test('cancelling reset to defaults modal preserves state', async ({ pageObjects }) => { const { featureSettings } = pageObjects; - await test.step('mock inference endpoints', async () => { - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); - await page.reload(); - await featureSettings.goto(); - }); - - const initialCount = await featureSettings.allEndpointRows.count(); - - await test.step('select a model from Alpha popover', async () => { - await featureSettings.addModelButton('test_feature_alpha').click(); - await expect(featureSettings.addModelSearch).toBeVisible(); - // eslint-disable-next-line playwright/no-nth-methods -- selecting the first available model from the popover list - await featureSettings.addModelOptions.first().click(); - }); - - await test.step('endpoint row count increases', async () => { - await expect(featureSettings.allEndpointRows).toHaveCount(initialCount + 1); - }); - - await test.step('save button becomes enabled', async () => { - await expect(featureSettings.saveButton).toBeEnabled(); - }); - }); - - test('reset to defaults modal cancel on Test Inference', async ({ pageObjects }) => { - const { featureSettings } = pageObjects; - - await test.step('click reset link opens confirmation modal', async () => { + await test.step('open reset to defaults modal', async () => { await featureSettings.resetLink('Test Inference').click(); await expect(featureSettings.resetDefaultsModal).toBeVisible(); }); - await test.step('cancel closes the modal without changes', async () => { + await test.step('cancel closes modal without changes', async () => { await featureSettings.resetDefaultsCancelButton.click(); await expect(featureSettings.resetDefaultsModal).toBeHidden(); await expect(featureSettings.saveButton).toBeDisabled(); }); }); - test('copy to modal cancel on Alpha', async ({ pageObjects }) => { + test('cancelling copy to modal closes without changes', async ({ pageObjects }) => { const { featureSettings } = pageObjects; - await test.step('click copy to on Alpha opens modal', async () => { + await test.step('open copy to modal', async () => { await featureSettings.copyToButton('test_feature_alpha').click(); await expect(featureSettings.copyToModalApply).toBeVisible(); await expect(featureSettings.copyToModalApply).toBeDisabled(); @@ -185,45 +143,107 @@ test.describe( }); }); - test('copy to from Alpha to Beta updates Beta endpoint list', async ({ page, pageObjects }) => { + test('searching in add model popover filters the results', async ({ page, pageObjects }) => { const { featureSettings } = pageObjects; + await setupMockEndpoints(page); + await featureSettings.goto(); + + try { + await test.step('open popover and verify models are listed', async () => { + await featureSettings.addModelButton('test_feature_alpha').click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + await expect(featureSettings.addModelOptions).not.toHaveCount(0); + }); + + await test.step('typing a search term reduces the list', async () => { + const countBeforeSearch = await featureSettings.addModelOptions.count(); + await featureSettings.addModelSearch.fill('anthropic'); + await expect + .poll(() => featureSettings.addModelOptions.count(), { + message: 'Expected filtered options to be fewer than initial', + }) + .toBeLessThan(countBeforeSearch); + await expect(featureSettings.addModelOptions).not.toHaveCount(0); + }); + } finally { + await page.unroute(MOCK_ROUTE); + } + }); - await test.step('mock inference endpoints', async () => { - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); - await page.reload(); - await featureSettings.goto(); - }); - - const betaCard = featureSettings.subFeatureCard('test_feature_beta'); - - await test.step('Beta contains its original endpoint before copy', async () => { - await expect(betaCard).toContainText('openai'); - await expect(betaCard).not.toContainText('anthropic'); - }); - - await test.step('open copy-to modal from Alpha', async () => { - await featureSettings.copyToButton('test_feature_alpha').click(); - await expect(featureSettings.copyToModalApply).toBeVisible(); - await expect(featureSettings.copyToModalApply).toBeDisabled(); - }); - - await test.step('select Beta as target', async () => { - await featureSettings.copyToModalCheckbox('test_feature_beta').click(); - }); - - await test.step('apply copies Alpha models to Beta', async () => { - await expect(featureSettings.copyToModalApply).toBeEnabled(); - await featureSettings.copyToModalApply.click(); - await expect(featureSettings.copyToModalApply).toBeHidden(); - }); - - await test.step('Beta now contains Alpha endpoint after copy', async () => { - await expect(betaCard).toContainText('anthropic'); - }); + test('selecting a model from the popover adds it to the endpoint list', async ({ + page, + pageObjects, + }) => { + const { featureSettings } = pageObjects; + await setupMockEndpoints(page); + await featureSettings.goto(); + + try { + const alphaRows = featureSettings.endpointRowsFor('test_feature_alpha'); + await expect(alphaRows).not.toHaveCount(0); + const initialCount = await alphaRows.count(); + + await test.step('open popover and select the first model', async () => { + await featureSettings.addModelButton('test_feature_alpha').click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + // eslint-disable-next-line playwright/no-nth-methods -- selecting the first available model from the popover list + await featureSettings.addModelOptions.first().click(); + }); + + await test.step('endpoint row count increases by one', async () => { + await expect(alphaRows).toHaveCount(initialCount + 1); + }); + + await test.step('save button becomes enabled', async () => { + await expect(featureSettings.saveButton).toBeEnabled(); + }); + } finally { + await page.unroute(MOCK_ROUTE); + } + }); - await test.step('save button becomes enabled after copy', async () => { - await expect(featureSettings.saveButton).toBeEnabled(); - }); + test('copy to applies source endpoint list to the target sub-feature', async ({ + page, + pageObjects, + }) => { + const { featureSettings } = pageObjects; + await setupMockEndpoints(page); + await featureSettings.goto(); + + try { + const betaCard = featureSettings.subFeatureCard('test_feature_beta'); + + await test.step('target sub-feature shows its original endpoint', async () => { + await expect(betaCard).toContainText('openai'); + await expect(betaCard).not.toContainText('anthropic'); + }); + + await test.step('open copy to modal from source sub-feature', async () => { + await featureSettings.copyToButton('test_feature_alpha').click(); + await expect(featureSettings.copyToModalApply).toBeVisible(); + await expect(featureSettings.copyToModalApply).toBeDisabled(); + }); + + await test.step('select target sub-feature', async () => { + await featureSettings.copyToModalCheckbox('test_feature_beta').click(); + }); + + await test.step('apply copies endpoints to target', async () => { + await expect(featureSettings.copyToModalApply).toBeEnabled(); + await featureSettings.copyToModalApply.click(); + await expect(featureSettings.copyToModalApply).toBeHidden(); + }); + + await test.step('target sub-feature now contains source endpoint', async () => { + await expect(betaCard).toContainText('anthropic'); + }); + + await test.step('save button becomes enabled', async () => { + await expect(featureSettings.saveButton).toBeEnabled(); + }); + } finally { + await page.unroute(MOCK_ROUTE); + } }); } ); From 248e78c2dacee9611f2b40628b9b6c41cff26ec9 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 13:52:40 -0600 Subject: [PATCH 23/34] Refactor code --- .../page_objects/feature_settings_page.ts | 16 +++++++++++ .../ui/tests/feature_settings.spec.ts | 28 +++++-------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts index ffd4f38ecd69f..d83c6122e5d86 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts @@ -127,4 +127,20 @@ export class FeatureSettingsPage { public get resetDefaultsCancelButton(): Locator { return this.resetDefaultsModal.locator('[data-test-subj="confirmModalCancelButton"]'); } + + // --- Route Mocking --- + + public async mockInferenceEndpoints(endpoints: unknown[]) { + await this.page.route('**/internal/inference_endpoints/endpoints', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ inference_endpoints: endpoints }), + }); + }); + } + + public async unmockInferenceEndpoints() { + await this.page.unroute('**/internal/inference_endpoints/endpoints'); + } } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index 63ef872d1c60c..2992dd9b92004 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -10,18 +10,6 @@ import { expect } from '@kbn/scout/ui'; import { test } from '../fixtures'; import { mockInferenceEndpoints } from '../fixtures/mock_data/inference_endpoints'; -const MOCK_ROUTE = '**/internal/inference_endpoints/endpoints'; - -async function setupMockEndpoints(page: Parameters[2]>[0]['page']) { - await page.route(MOCK_ROUTE, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ inference_endpoints: mockInferenceEndpoints }), - }); - }); -} - test.describe( 'Feature Settings', { tag: [...tags.stateful.classic, ...tags.serverless.search] }, @@ -143,9 +131,9 @@ test.describe( }); }); - test('searching in add model popover filters the results', async ({ page, pageObjects }) => { + test('searching in add model popover filters the results', async ({ pageObjects }) => { const { featureSettings } = pageObjects; - await setupMockEndpoints(page); + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); await featureSettings.goto(); try { @@ -166,16 +154,15 @@ test.describe( await expect(featureSettings.addModelOptions).not.toHaveCount(0); }); } finally { - await page.unroute(MOCK_ROUTE); + await featureSettings.unmockInferenceEndpoints(); } }); test('selecting a model from the popover adds it to the endpoint list', async ({ - page, pageObjects, }) => { const { featureSettings } = pageObjects; - await setupMockEndpoints(page); + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); await featureSettings.goto(); try { @@ -198,16 +185,15 @@ test.describe( await expect(featureSettings.saveButton).toBeEnabled(); }); } finally { - await page.unroute(MOCK_ROUTE); + await featureSettings.unmockInferenceEndpoints(); } }); test('copy to applies source endpoint list to the target sub-feature', async ({ - page, pageObjects, }) => { const { featureSettings } = pageObjects; - await setupMockEndpoints(page); + await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); await featureSettings.goto(); try { @@ -242,7 +228,7 @@ test.describe( await expect(featureSettings.saveButton).toBeEnabled(); }); } finally { - await page.unroute(MOCK_ROUTE); + await featureSettings.unmockInferenceEndpoints(); } }); } From 29e184315e9739f8e3224e0819774f0436e4f954 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 14:16:16 -0600 Subject: [PATCH 24/34] Refactor code --- .../scout_inference_test/ui/tests/feature_settings.spec.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index 2992dd9b92004..92ec6173ca9cb 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -146,11 +146,7 @@ test.describe( await test.step('typing a search term reduces the list', async () => { const countBeforeSearch = await featureSettings.addModelOptions.count(); await featureSettings.addModelSearch.fill('anthropic'); - await expect - .poll(() => featureSettings.addModelOptions.count(), { - message: 'Expected filtered options to be fewer than initial', - }) - .toBeLessThan(countBeforeSearch); + await expect(featureSettings.addModelOptions).not.toHaveCount(countBeforeSearch); await expect(featureSettings.addModelOptions).not.toHaveCount(0); }); } finally { From 9dcdde66a2c21e80eee97a3d2d90301fd95ed2f9 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 15:28:41 -0600 Subject: [PATCH 25/34] Refactor code --- .../ui/fixtures/page_objects/feature_settings_page.ts | 9 ++++++--- .../ui/tests/feature_settings.spec.ts | 11 ++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts index d83c6122e5d86..9b042a622c486 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts @@ -69,9 +69,8 @@ export class FeatureSettingsPage { return this.subFeatureCard(featureId).locator('[data-test-subj^="endpoint-row-"]'); } - public get firstEndpointRow(): Locator { - // eslint-disable-next-line playwright/no-nth-methods -- selecting the first endpoint row to verify default badge presence - return this.allEndpointRows.first(); + public endpointRow(endpointId: string): Locator { + return this.page.testSubj.locator(`endpoint-row-${endpointId}`); } // --- Sub-Feature Card by ID --- @@ -96,6 +95,10 @@ export class FeatureSettingsPage { return this.page.testSubj.locator('add-model-selectable').getByRole('option'); } + public addModelOption(name: string): Locator { + return this.page.testSubj.locator('add-model-selectable').getByRole('option', { name }); + } + // --- Copy To Modal --- public copyToButton(featureId: string): Locator { diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index 92ec6173ca9cb..1d4826c332208 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -75,8 +75,10 @@ test.describe( await expect(featureSettings.allEndpointRows).not.toHaveCount(0); }); - await test.step('first endpoint row has a default badge', async () => { - await expect(featureSettings.firstEndpointRow).toContainText('Default'); + await test.step('fixture endpoint row has a default badge', async () => { + await expect( + featureSettings.endpointRow('.anthropic-claude-3.7-sonnet-chat_completion') + ).toContainText('Default'); }); }); @@ -166,11 +168,10 @@ test.describe( await expect(alphaRows).not.toHaveCount(0); const initialCount = await alphaRows.count(); - await test.step('open popover and select the first model', async () => { + await test.step('open popover and select a model', async () => { await featureSettings.addModelButton('test_feature_alpha').click(); await expect(featureSettings.addModelSearch).toBeVisible(); - // eslint-disable-next-line playwright/no-nth-methods -- selecting the first available model from the popover list - await featureSettings.addModelOptions.first().click(); + await featureSettings.addModelOption('anthropic').click(); }); await test.step('endpoint row count increases by one', async () => { From c8e8bc4b92023ab01cdc74fc72c69a66731378d5 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Thu, 9 Apr 2026 15:39:28 -0600 Subject: [PATCH 26/34] Refactor code --- .agents/skills/scout-ui-testing/SKILL.md | 1 + .../ui/fixtures/page_objects/feature_settings_page.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.agents/skills/scout-ui-testing/SKILL.md b/.agents/skills/scout-ui-testing/SKILL.md index d0e61d7781e27..fcb7ce771a471 100644 --- a/.agents/skills/scout-ui-testing/SKILL.md +++ b/.agents/skills/scout-ui-testing/SKILL.md @@ -42,6 +42,7 @@ description: Use when creating, updating, debugging, or reviewing Scout UI tests - Don't make API calls from page objects (use `apiServices`/`kbnClient` in hooks instead). - Register plugin page objects by extending the `pageObjects` fixture in `test/scout*/ui/fixtures/index.ts`. - Scout provides EUI component wrappers for stable interactions with common EUI widgets: `EuiComboBoxWrapper`, `EuiDataGridWrapper`, `EuiSelectableWrapper`, `EuiCheckBoxWrapper`, `EuiFieldTextWrapper`, `EuiCodeBlockWrapper`, `EuiSuperSelectWrapper`, `EuiToastWrapper`. Import them from `@kbn/scout` and use them as class members in page objects. +- **Avoid `.first()`, `.nth()`, `.last()`** — the `playwright/no-nth-methods` lint rule flags these. Instead, use `data-test-subj` attributes, `getByRole('option', { name })`, or other targeted selectors. If the production component lacks a `data-test-subj`, add one rather than disabling the rule. ## Parallel UI specifics (spaceTest) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts index 9b042a622c486..346fff9d664dc 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts @@ -6,13 +6,14 @@ */ import type { ScoutPage, Locator } from '@kbn/scout'; +import { expect } from '@kbn/scout/ui'; export class FeatureSettingsPage { constructor(private readonly page: ScoutPage) {} public async goto() { await this.page.gotoApp('management/modelManagement/model_settings'); - await this.page.testSubj.waitForSelector('modelSettingsPage'); + await expect(this.page.testSubj.locator('modelSettingsPage')).toBeVisible(); } // --- Header --- From 67549732657366bd03e69953fe997af65f9ffe24 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Fri, 10 Apr 2026 07:53:49 -0600 Subject: [PATCH 27/34] Refactor code --- .../page_objects/feature_settings_page.ts | 16 +++++++++++++++- .../ui/tests/feature_settings.spec.ts | 5 +++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts index 346fff9d664dc..146b276645722 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts @@ -13,7 +13,7 @@ export class FeatureSettingsPage { public async goto() { await this.page.gotoApp('management/modelManagement/model_settings'); - await expect(this.page.testSubj.locator('modelSettingsPage')).toBeVisible(); + await expect(this.page.testSubj.locator('modelSettingsPageHeader')).toBeVisible(); } // --- Header --- @@ -134,6 +134,20 @@ export class FeatureSettingsPage { // --- Route Mocking --- + public async mockConnectors() { + await this.page.route('**/internal/inference/connectors', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ connectors: [{ connectorId: 'mock-connector' }] }), + }); + }); + } + + public async unmockConnectors() { + await this.page.unroute('**/internal/inference/connectors'); + } + public async mockInferenceEndpoints(endpoints: unknown[]) { await this.page.route('**/internal/inference_endpoints/endpoints', async (route) => { await route.fulfill({ diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index 1d4826c332208..4c22ecc375986 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -16,9 +16,14 @@ test.describe( () => { test.beforeEach(async ({ browserAuth, pageObjects }) => { await browserAuth.loginAsPrivilegedUser(); + await pageObjects.featureSettings.mockConnectors(); await pageObjects.featureSettings.goto(); }); + test.afterEach(async ({ pageObjects }) => { + await pageObjects.featureSettings.unmockConnectors(); + }); + test('displays page header', async ({ pageObjects }) => { await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); }); From e39a2dc6a44b21f1f76a617fb358d292e1753dcc Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Fri, 10 Apr 2026 08:42:48 -0600 Subject: [PATCH 28/34] Skip MKI --- .../fixtures/page_objects/feature_settings_page.ts | 13 ++++++++++++- .../ui/tests/feature_settings.spec.ts | 3 +-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts index 146b276645722..1432bbea2ce18 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts @@ -139,7 +139,18 @@ export class FeatureSettingsPage { await route.fulfill({ status: 200, contentType: 'application/json', - body: JSON.stringify({ connectors: [{ connectorId: 'mock-connector' }] }), + body: JSON.stringify({ + connectors: [ + { + connectorId: 'mock-connector', + name: 'Mock Connector', + type: '.gen-ai', + config: {}, + capabilities: {}, + isPreconfigured: false, + }, + ], + }), }); }); } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index 4c22ecc375986..df13c799bc831 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -5,14 +5,13 @@ * 2.0. */ -import { tags } from '@kbn/scout'; import { expect } from '@kbn/scout/ui'; import { test } from '../fixtures'; import { mockInferenceEndpoints } from '../fixtures/mock_data/inference_endpoints'; test.describe( 'Feature Settings', - { tag: [...tags.stateful.classic, ...tags.serverless.search] }, + { tag: ['@local-stateful-classic', '@local-serverless-search'] }, () => { test.beforeEach(async ({ browserAuth, pageObjects }) => { await browserAuth.loginAsPrivilegedUser(); From ca516e868ed092109590ccabd09edb8cba1eaa1e Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Fri, 10 Apr 2026 08:46:28 -0600 Subject: [PATCH 29/34] Refactor code --- .agents/skills/scout-ui-testing/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/scout-ui-testing/SKILL.md b/.agents/skills/scout-ui-testing/SKILL.md index fcb7ce771a471..2a5607dca41cd 100644 --- a/.agents/skills/scout-ui-testing/SKILL.md +++ b/.agents/skills/scout-ui-testing/SKILL.md @@ -42,7 +42,7 @@ description: Use when creating, updating, debugging, or reviewing Scout UI tests - Don't make API calls from page objects (use `apiServices`/`kbnClient` in hooks instead). - Register plugin page objects by extending the `pageObjects` fixture in `test/scout*/ui/fixtures/index.ts`. - Scout provides EUI component wrappers for stable interactions with common EUI widgets: `EuiComboBoxWrapper`, `EuiDataGridWrapper`, `EuiSelectableWrapper`, `EuiCheckBoxWrapper`, `EuiFieldTextWrapper`, `EuiCodeBlockWrapper`, `EuiSuperSelectWrapper`, `EuiToastWrapper`. Import them from `@kbn/scout` and use them as class members in page objects. -- **Avoid `.first()`, `.nth()`, `.last()`** — the `playwright/no-nth-methods` lint rule flags these. Instead, use `data-test-subj` attributes, `getByRole('option', { name })`, or other targeted selectors. If the production component lacks a `data-test-subj`, add one rather than disabling the rule. +- **Avoid `.first()`, `.nth()`, `.last()`** — the `playwright/no-nth-methods` lint rule flags these. Instead, use `data-test-subj` attributes or other targeted selectors. If the component lacks a `data-test-subj`, add one rather than disabling the rule. ## Parallel UI specifics (spaceTest) From 798d2290df7184b891367da016961e5bd1e70216 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Fri, 10 Apr 2026 09:01:59 -0600 Subject: [PATCH 30/34] Refactor code --- .../page_objects/feature_settings_page.ts | 25 +++++++++++++++++++ .../ui/tests/feature_settings.spec.ts | 15 +++++++++++ 2 files changed, 40 insertions(+) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts index 1432bbea2ce18..57fa51e3f78df 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts @@ -132,6 +132,21 @@ export class FeatureSettingsPage { return this.resetDefaultsModal.locator('[data-test-subj="confirmModalCancelButton"]'); } + // --- Empty State --- + + public get noModelsEmptyPrompt(): Locator { + return this.page.testSubj.locator('settings-no-models'); + } + + public get addModelsButton(): Locator { + return this.page.testSubj.locator('settings-no-models-add-models'); + } + + public async gotoEmptyState() { + await this.page.gotoApp('management/modelManagement/model_settings'); + await expect(this.noModelsEmptyPrompt).toBeVisible(); + } + // --- Route Mocking --- public async mockConnectors() { @@ -155,6 +170,16 @@ export class FeatureSettingsPage { }); } + public async mockEmptyConnectors() { + await this.page.route('**/internal/inference/connectors', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ connectors: [] }), + }); + }); + } + public async unmockConnectors() { await this.page.unroute('**/internal/inference/connectors'); } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index df13c799bc831..afdfb5502e99f 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -23,6 +23,21 @@ test.describe( await pageObjects.featureSettings.unmockConnectors(); }); + test('shows empty state when no models are available', async ({ pageObjects }) => { + const { featureSettings } = pageObjects; + await featureSettings.unmockConnectors(); + await featureSettings.mockEmptyConnectors(); + await featureSettings.gotoEmptyState(); + + try { + await expect(featureSettings.noModelsEmptyPrompt).toBeVisible(); + await expect(featureSettings.addModelsButton).toBeVisible(); + } finally { + await featureSettings.unmockConnectors(); + await featureSettings.mockConnectors(); + } + }); + test('displays page header', async ({ pageObjects }) => { await expect(pageObjects.featureSettings.pageHeader).toBeVisible(); }); From c7d85fe8ba4c95b4386512cf956daef9d092243d Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Fri, 10 Apr 2026 10:14:56 -0600 Subject: [PATCH 31/34] Refactor code --- .../test/scout_inference_test/ui/tests/feature_settings.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index afdfb5502e99f..83c03cdde3972 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -14,8 +14,8 @@ test.describe( { tag: ['@local-stateful-classic', '@local-serverless-search'] }, () => { test.beforeEach(async ({ browserAuth, pageObjects }) => { - await browserAuth.loginAsPrivilegedUser(); await pageObjects.featureSettings.mockConnectors(); + await browserAuth.loginAsPrivilegedUser(); await pageObjects.featureSettings.goto(); }); From 5a7c34f0f35a1bdeff3df007072b7faf38a989a1 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Fri, 10 Apr 2026 10:17:36 -0600 Subject: [PATCH 32/34] Refactor code --- .agents/skills/scout-ui-testing/SKILL.md | 1 + .../test/scout_inference_test/ui/tests/feature_settings.spec.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.agents/skills/scout-ui-testing/SKILL.md b/.agents/skills/scout-ui-testing/SKILL.md index 2a5607dca41cd..811e93349b1ca 100644 --- a/.agents/skills/scout-ui-testing/SKILL.md +++ b/.agents/skills/scout-ui-testing/SKILL.md @@ -43,6 +43,7 @@ description: Use when creating, updating, debugging, or reviewing Scout UI tests - Register plugin page objects by extending the `pageObjects` fixture in `test/scout*/ui/fixtures/index.ts`. - Scout provides EUI component wrappers for stable interactions with common EUI widgets: `EuiComboBoxWrapper`, `EuiDataGridWrapper`, `EuiSelectableWrapper`, `EuiCheckBoxWrapper`, `EuiFieldTextWrapper`, `EuiCodeBlockWrapper`, `EuiSuperSelectWrapper`, `EuiToastWrapper`. Import them from `@kbn/scout` and use them as class members in page objects. - **Avoid `.first()`, `.nth()`, `.last()`** — the `playwright/no-nth-methods` lint rule flags these. Instead, use `data-test-subj` attributes or other targeted selectors. If the component lacks a `data-test-subj`, add one rather than disabling the rule. +- **Do not disable eslint rules** — avoid `eslint-disable` comments in test files. Fix the underlying issue (e.g., use targeted selectors instead of positional ones, add `data-test-subj` to the components) rather than suppressing the lint rule. ## Parallel UI specifics (spaceTest) diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index 83c03cdde3972..fb59862f4ecd8 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -11,7 +11,7 @@ import { mockInferenceEndpoints } from '../fixtures/mock_data/inference_endpoint test.describe( 'Feature Settings', - { tag: ['@local-stateful-classic', '@local-serverless-search'] }, + { tag: ['@local-stateful-classic', '@local-stateful-search', '@local-serverless-search'] }, () => { test.beforeEach(async ({ browserAuth, pageObjects }) => { await pageObjects.featureSettings.mockConnectors(); From 30d464a2271e96fc8382b10fdbd0e9448637bd59 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Mon, 13 Apr 2026 15:25:06 -0600 Subject: [PATCH 33/34] Add readonly class fields --- .agents/skills/scout-ui-testing/SKILL.md | 1 + .../page_objects/feature_settings_page.ts | 162 ++++++++---------- 2 files changed, 74 insertions(+), 89 deletions(-) diff --git a/.agents/skills/scout-ui-testing/SKILL.md b/.agents/skills/scout-ui-testing/SKILL.md index 811e93349b1ca..4f84e1f71d7f4 100644 --- a/.agents/skills/scout-ui-testing/SKILL.md +++ b/.agents/skills/scout-ui-testing/SKILL.md @@ -41,6 +41,7 @@ description: Use when creating, updating, debugging, or reviewing Scout UI tests - Keep selectors + interactions inside the page object class. - Don't make API calls from page objects (use `apiServices`/`kbnClient` in hooks instead). - Register plugin page objects by extending the `pageObjects` fixture in `test/scout*/ui/fixtures/index.ts`. +- **Use `readonly` class fields for static locators** — assign them in the constructor, not as getter methods. Use methods only for parameterized locators/actions. See `DashboardApp` in `kbn-scout` for the reference pattern. - Scout provides EUI component wrappers for stable interactions with common EUI widgets: `EuiComboBoxWrapper`, `EuiDataGridWrapper`, `EuiSelectableWrapper`, `EuiCheckBoxWrapper`, `EuiFieldTextWrapper`, `EuiCodeBlockWrapper`, `EuiSuperSelectWrapper`, `EuiToastWrapper`. Import them from `@kbn/scout` and use them as class members in page objects. - **Avoid `.first()`, `.nth()`, `.last()`** — the `playwright/no-nth-methods` lint rule flags these. Instead, use `data-test-subj` attributes or other targeted selectors. If the component lacks a `data-test-subj`, add one rather than disabling the rule. - **Do not disable eslint rules** — avoid `eslint-disable` comments in test files. Fix the underlying issue (e.g., use targeted selectors instead of positional ones, add `data-test-subj` to the components) rather than suppressing the lint rule. diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts index 57fa51e3f78df..b021840a16365 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts @@ -9,99 +9,116 @@ import type { ScoutPage, Locator } from '@kbn/scout'; import { expect } from '@kbn/scout/ui'; export class FeatureSettingsPage { - constructor(private readonly page: ScoutPage) {} + // Header + readonly pageHeader: Locator; + readonly saveButton: Locator; + readonly apiDocumentationLink: Locator; - public async goto() { - await this.page.gotoApp('management/modelManagement/model_settings'); - await expect(this.page.testSubj.locator('modelSettingsPageHeader')).toBeVisible(); - } + // Content + readonly content: Locator; - // --- Header --- + // Default Model Section + readonly defaultModelSection: Locator; + readonly defaultModelComboBox: Locator; + readonly disallowOtherModelsCheckbox: Locator; - public get pageHeader(): Locator { - return this.page.testSubj.locator('modelSettingsPageHeader'); - } + // Feature Sections + readonly allFeatureSections: Locator; - public get saveButton(): Locator { - return this.page.testSubj.locator('save-settings-button'); - } + // Sub-Feature Cards + readonly allSubFeatureCards: Locator; + readonly allEndpointRows: Locator; - public get apiDocumentationLink(): Locator { - return this.page.testSubj.locator('settings-api-documentation'); - } + // Add Model Popover + readonly addModelSearch: Locator; + readonly addModelOptions: Locator; - // --- Content --- + // Copy To Modal + readonly copyToModalApply: Locator; + readonly copyToModalCancel: Locator; - public get content(): Locator { - return this.page.testSubj.locator('modelSettingsContent'); - } + // Reset Defaults Modal + readonly resetDefaultsModal: Locator; + readonly resetDefaultsCancelButton: Locator; - // --- Default Model Section --- + // Empty State + readonly noModelsEmptyPrompt: Locator; + readonly addModelsButton: Locator; - public get defaultModelSection(): Locator { - return this.page.testSubj.locator('defaultModelSection'); - } + constructor(private readonly page: ScoutPage) { + // Header + this.pageHeader = this.page.testSubj.locator('modelSettingsPageHeader'); + this.saveButton = this.page.testSubj.locator('save-settings-button'); + this.apiDocumentationLink = this.page.testSubj.locator('settings-api-documentation'); - public get defaultModelComboBox(): Locator { - return this.page.testSubj.locator('defaultModelComboBox'); - } + // Content + this.content = this.page.testSubj.locator('modelSettingsContent'); - public get disallowOtherModelsCheckbox(): Locator { - return this.page.testSubj.locator('disallowOtherModelsCheckbox'); - } + // Default Model Section + this.defaultModelSection = this.page.testSubj.locator('defaultModelSection'); + this.defaultModelComboBox = this.page.testSubj.locator('defaultModelComboBox'); + this.disallowOtherModelsCheckbox = this.page.testSubj.locator('disallowOtherModelsCheckbox'); - // --- Feature Sections --- + // Feature Sections + this.allFeatureSections = this.content.locator('[data-test-subj^="featureSection-"]'); - public get allFeatureSections(): Locator { - return this.content.locator('[data-test-subj^="featureSection-"]'); - } + // Sub-Feature Cards + this.allSubFeatureCards = this.content.locator('[data-test-subj^="subFeatureCard-"]'); + this.allEndpointRows = this.content.locator('[data-test-subj^="endpoint-row-"]'); - // --- Sub-Feature Cards --- + // Add Model Popover + this.addModelSearch = this.page.testSubj.locator('add-model-search'); + this.addModelOptions = this.page.testSubj.locator('add-model-selectable').getByRole('option'); - public get allSubFeatureCards(): Locator { - return this.content.locator('[data-test-subj^="subFeatureCard-"]'); - } + // Copy To Modal + this.copyToModalApply = this.page.testSubj.locator('copy-to-modal-apply'); + this.copyToModalCancel = this.page.testSubj.locator('copy-to-modal-cancel'); + + // Reset Defaults Modal + this.resetDefaultsModal = this.page.testSubj.locator('resetDefaultsModal'); + this.resetDefaultsCancelButton = this.resetDefaultsModal.locator( + '[data-test-subj="confirmModalCancelButton"]' + ); - public get allEndpointRows(): Locator { - return this.content.locator('[data-test-subj^="endpoint-row-"]'); + // Empty State + this.noModelsEmptyPrompt = this.page.testSubj.locator('settings-no-models'); + this.addModelsButton = this.page.testSubj.locator('settings-no-models-add-models'); } - public endpointRowsFor(featureId: string): Locator { - return this.subFeatureCard(featureId).locator('[data-test-subj^="endpoint-row-"]'); + // --- Navigation --- + + public async goto() { + await this.page.gotoApp('management/modelManagement/model_settings'); + await expect(this.pageHeader).toBeVisible(); } - public endpointRow(endpointId: string): Locator { - return this.page.testSubj.locator(`endpoint-row-${endpointId}`); + public async gotoEmptyState() { + await this.page.gotoApp('management/modelManagement/model_settings'); + await expect(this.noModelsEmptyPrompt).toBeVisible(); } - // --- Sub-Feature Card by ID --- + // --- Parameterized Locators --- public subFeatureCard(featureId: string): Locator { return this.page.testSubj.locator(`subFeatureCard-${featureId}`); } - // --- Add Model Popover --- - - public addModelButton(featureId: string): Locator { - return this.subFeatureCard(featureId).locator('[data-test-subj="add-model-button"]'); + public endpointRowsFor(featureId: string): Locator { + return this.subFeatureCard(featureId).locator('[data-test-subj^="endpoint-row-"]'); } - public get addModelSearch(): Locator { - return this.page.testSubj.locator('add-model-search'); + public endpointRow(endpointId: string): Locator { + return this.page.testSubj.locator(`endpoint-row-${endpointId}`); } - // --- Add Model Popover Options --- - - public get addModelOptions(): Locator { - return this.page.testSubj.locator('add-model-selectable').getByRole('option'); + public addModelButton(featureId: string): Locator { + return this.subFeatureCard(featureId).locator('[data-test-subj="add-model-button"]'); } public addModelOption(name: string): Locator { return this.page.testSubj.locator('add-model-selectable').getByRole('option', { name }); } - // --- Copy To Modal --- - public copyToButton(featureId: string): Locator { return this.page.testSubj.locator(`copy-to-${featureId}`); } @@ -110,43 +127,10 @@ export class FeatureSettingsPage { return this.page.locator(`#copy-target-${featureId}`); } - public get copyToModalApply(): Locator { - return this.page.testSubj.locator('copy-to-modal-apply'); - } - - public get copyToModalCancel(): Locator { - return this.page.testSubj.locator('copy-to-modal-cancel'); - } - - // --- Reset Defaults Modal --- - public resetLink(parentName: string): Locator { return this.page.testSubj.locator(`reset-${parentName}`); } - public get resetDefaultsModal(): Locator { - return this.page.testSubj.locator('resetDefaultsModal'); - } - - public get resetDefaultsCancelButton(): Locator { - return this.resetDefaultsModal.locator('[data-test-subj="confirmModalCancelButton"]'); - } - - // --- Empty State --- - - public get noModelsEmptyPrompt(): Locator { - return this.page.testSubj.locator('settings-no-models'); - } - - public get addModelsButton(): Locator { - return this.page.testSubj.locator('settings-no-models-add-models'); - } - - public async gotoEmptyState() { - await this.page.gotoApp('management/modelManagement/model_settings'); - await expect(this.noModelsEmptyPrompt).toBeVisible(); - } - // --- Route Mocking --- public async mockConnectors() { From f45cd45676c8a628943ae1d9e0e89f849cade57c Mon Sep 17 00:00:00 2001 From: Saikat Sarkar Date: Mon, 13 Apr 2026 15:42:20 -0600 Subject: [PATCH 34/34] Refactor code --- .agents/skills/scout-ui-testing/SKILL.md | 3 +- .../scout_inference_test/ui/fixtures/mocks.ts | 60 ++++++ .../page_objects/feature_settings_page.ts | 56 +----- .../ui/tests/feature_settings.spec.ts | 173 +++++++++--------- 4 files changed, 147 insertions(+), 145 deletions(-) create mode 100644 x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/mocks.ts diff --git a/.agents/skills/scout-ui-testing/SKILL.md b/.agents/skills/scout-ui-testing/SKILL.md index 4f84e1f71d7f4..530f20eba79ce 100644 --- a/.agents/skills/scout-ui-testing/SKILL.md +++ b/.agents/skills/scout-ui-testing/SKILL.md @@ -38,7 +38,8 @@ description: Use when creating, updating, debugging, or reviewing Scout UI tests ## Page objects (UI) - Prefer `page.testSubj.locator(...)`, role/label locators; avoid brittle CSS. -- Keep selectors + interactions inside the page object class. +- Keep selectors + interactions inside the page object class. **Do not use `expect` assertions in page objects** — use `waitForSelector` for waiting on elements. Assertions belong in test specs only. +- **Keep route mocks out of page objects** — page objects are for UI interactions only. Put `page.route()` mocks in a dedicated `fixtures/mocks.ts` file as standalone functions that accept `page` as a parameter. See `cloud_security_posture/test/scout_cspm_agentless/ui/fixtures/mocks.ts` for the reference pattern. - Don't make API calls from page objects (use `apiServices`/`kbnClient` in hooks instead). - Register plugin page objects by extending the `pageObjects` fixture in `test/scout*/ui/fixtures/index.ts`. - **Use `readonly` class fields for static locators** — assign them in the constructor, not as getter methods. Use methods only for parameterized locators/actions. See `DashboardApp` in `kbn-scout` for the reference pattern. diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/mocks.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/mocks.ts new file mode 100644 index 0000000000000..d2e3e8b6d24c1 --- /dev/null +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/mocks.ts @@ -0,0 +1,60 @@ +/* + * 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 type { ScoutPage } from '@kbn/scout'; + +const CONNECTORS_ROUTE = '**/internal/inference/connectors'; +const ENDPOINTS_ROUTE = '**/internal/inference_endpoints/endpoints'; + +export async function mockConnectors(page: ScoutPage) { + await page.route(CONNECTORS_ROUTE, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + connectors: [ + { + connectorId: 'mock-connector', + name: 'Mock Connector', + type: '.gen-ai', + config: {}, + capabilities: {}, + isPreconfigured: false, + }, + ], + }), + }); + }); +} + +export async function mockEmptyConnectors(page: ScoutPage) { + await page.route(CONNECTORS_ROUTE, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ connectors: [] }), + }); + }); +} + +export async function unmockConnectors(page: ScoutPage) { + await page.unroute(CONNECTORS_ROUTE); +} + +export async function mockInferenceEndpoints(page: ScoutPage, endpoints: unknown[]) { + await page.route(ENDPOINTS_ROUTE, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ inference_endpoints: endpoints }), + }); + }); +} + +export async function unmockInferenceEndpoints(page: ScoutPage) { + await page.unroute(ENDPOINTS_ROUTE); +} diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts index b021840a16365..618d312234457 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/fixtures/page_objects/feature_settings_page.ts @@ -6,7 +6,6 @@ */ import type { ScoutPage, Locator } from '@kbn/scout'; -import { expect } from '@kbn/scout/ui'; export class FeatureSettingsPage { // Header @@ -89,12 +88,12 @@ export class FeatureSettingsPage { public async goto() { await this.page.gotoApp('management/modelManagement/model_settings'); - await expect(this.pageHeader).toBeVisible(); + await this.page.testSubj.waitForSelector('modelSettingsPageHeader', { state: 'visible' }); } public async gotoEmptyState() { await this.page.gotoApp('management/modelManagement/model_settings'); - await expect(this.noModelsEmptyPrompt).toBeVisible(); + await this.page.testSubj.waitForSelector('settings-no-models', { state: 'visible' }); } // --- Parameterized Locators --- @@ -130,55 +129,4 @@ export class FeatureSettingsPage { public resetLink(parentName: string): Locator { return this.page.testSubj.locator(`reset-${parentName}`); } - - // --- Route Mocking --- - - public async mockConnectors() { - await this.page.route('**/internal/inference/connectors', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - connectors: [ - { - connectorId: 'mock-connector', - name: 'Mock Connector', - type: '.gen-ai', - config: {}, - capabilities: {}, - isPreconfigured: false, - }, - ], - }), - }); - }); - } - - public async mockEmptyConnectors() { - await this.page.route('**/internal/inference/connectors', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ connectors: [] }), - }); - }); - } - - public async unmockConnectors() { - await this.page.unroute('**/internal/inference/connectors'); - } - - public async mockInferenceEndpoints(endpoints: unknown[]) { - await this.page.route('**/internal/inference_endpoints/endpoints', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ inference_endpoints: endpoints }), - }); - }); - } - - public async unmockInferenceEndpoints() { - await this.page.unroute('**/internal/inference_endpoints/endpoints'); - } } diff --git a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts index fb59862f4ecd8..f5fcc0d025f77 100644 --- a/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts +++ b/x-pack/platform/plugins/shared/search_inference_endpoints/test/scout_inference_test/ui/tests/feature_settings.spec.ts @@ -7,35 +7,38 @@ import { expect } from '@kbn/scout/ui'; import { test } from '../fixtures'; -import { mockInferenceEndpoints } from '../fixtures/mock_data/inference_endpoints'; +import { mockInferenceEndpoints as mockEndpointsData } from '../fixtures/mock_data/inference_endpoints'; +import { + mockConnectors, + mockEmptyConnectors, + unmockConnectors, + mockInferenceEndpoints, + unmockInferenceEndpoints, +} from '../fixtures/mocks'; test.describe( 'Feature Settings', { tag: ['@local-stateful-classic', '@local-stateful-search', '@local-serverless-search'] }, () => { - test.beforeEach(async ({ browserAuth, pageObjects }) => { - await pageObjects.featureSettings.mockConnectors(); + test.beforeEach(async ({ browserAuth, page, pageObjects }) => { + await mockConnectors(page); await browserAuth.loginAsPrivilegedUser(); await pageObjects.featureSettings.goto(); }); - test.afterEach(async ({ pageObjects }) => { - await pageObjects.featureSettings.unmockConnectors(); + test.afterEach(async ({ page }) => { + await unmockConnectors(page); + await unmockInferenceEndpoints(page); }); - test('shows empty state when no models are available', async ({ pageObjects }) => { + test('shows empty state when no models are available', async ({ page, pageObjects }) => { const { featureSettings } = pageObjects; - await featureSettings.unmockConnectors(); - await featureSettings.mockEmptyConnectors(); + await unmockConnectors(page); + await mockEmptyConnectors(page); await featureSettings.gotoEmptyState(); - try { - await expect(featureSettings.noModelsEmptyPrompt).toBeVisible(); - await expect(featureSettings.addModelsButton).toBeVisible(); - } finally { - await featureSettings.unmockConnectors(); - await featureSettings.mockConnectors(); - } + await expect(featureSettings.noModelsEmptyPrompt).toBeVisible(); + await expect(featureSettings.addModelsButton).toBeVisible(); }); test('displays page header', async ({ pageObjects }) => { @@ -152,100 +155,90 @@ test.describe( }); }); - test('searching in add model popover filters the results', async ({ pageObjects }) => { + test('searching in add model popover filters the results', async ({ page, pageObjects }) => { const { featureSettings } = pageObjects; - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await mockInferenceEndpoints(page, mockEndpointsData); await featureSettings.goto(); - try { - await test.step('open popover and verify models are listed', async () => { - await featureSettings.addModelButton('test_feature_alpha').click(); - await expect(featureSettings.addModelSearch).toBeVisible(); - await expect(featureSettings.addModelOptions).not.toHaveCount(0); - }); - - await test.step('typing a search term reduces the list', async () => { - const countBeforeSearch = await featureSettings.addModelOptions.count(); - await featureSettings.addModelSearch.fill('anthropic'); - await expect(featureSettings.addModelOptions).not.toHaveCount(countBeforeSearch); - await expect(featureSettings.addModelOptions).not.toHaveCount(0); - }); - } finally { - await featureSettings.unmockInferenceEndpoints(); - } + await test.step('open popover and verify models are listed', async () => { + await featureSettings.addModelButton('test_feature_alpha').click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + await expect(featureSettings.addModelOptions).not.toHaveCount(0); + }); + + await test.step('typing a search term reduces the list', async () => { + const countBeforeSearch = await featureSettings.addModelOptions.count(); + await featureSettings.addModelSearch.fill('anthropic'); + await expect(featureSettings.addModelOptions).not.toHaveCount(countBeforeSearch); + await expect(featureSettings.addModelOptions).not.toHaveCount(0); + }); }); test('selecting a model from the popover adds it to the endpoint list', async ({ + page, pageObjects, }) => { const { featureSettings } = pageObjects; - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await mockInferenceEndpoints(page, mockEndpointsData); await featureSettings.goto(); - try { - const alphaRows = featureSettings.endpointRowsFor('test_feature_alpha'); - await expect(alphaRows).not.toHaveCount(0); - const initialCount = await alphaRows.count(); - - await test.step('open popover and select a model', async () => { - await featureSettings.addModelButton('test_feature_alpha').click(); - await expect(featureSettings.addModelSearch).toBeVisible(); - await featureSettings.addModelOption('anthropic').click(); - }); - - await test.step('endpoint row count increases by one', async () => { - await expect(alphaRows).toHaveCount(initialCount + 1); - }); - - await test.step('save button becomes enabled', async () => { - await expect(featureSettings.saveButton).toBeEnabled(); - }); - } finally { - await featureSettings.unmockInferenceEndpoints(); - } + const alphaRows = featureSettings.endpointRowsFor('test_feature_alpha'); + await expect(alphaRows).not.toHaveCount(0); + const initialCount = await alphaRows.count(); + + await test.step('open popover and select a model', async () => { + await featureSettings.addModelButton('test_feature_alpha').click(); + await expect(featureSettings.addModelSearch).toBeVisible(); + await featureSettings.addModelOption('anthropic').click(); + }); + + await test.step('endpoint row count increases by one', async () => { + await expect(alphaRows).toHaveCount(initialCount + 1); + }); + + await test.step('save button becomes enabled', async () => { + await expect(featureSettings.saveButton).toBeEnabled(); + }); }); test('copy to applies source endpoint list to the target sub-feature', async ({ + page, pageObjects, }) => { const { featureSettings } = pageObjects; - await featureSettings.mockInferenceEndpoints(mockInferenceEndpoints); + await mockInferenceEndpoints(page, mockEndpointsData); await featureSettings.goto(); - try { - const betaCard = featureSettings.subFeatureCard('test_feature_beta'); - - await test.step('target sub-feature shows its original endpoint', async () => { - await expect(betaCard).toContainText('openai'); - await expect(betaCard).not.toContainText('anthropic'); - }); - - await test.step('open copy to modal from source sub-feature', async () => { - await featureSettings.copyToButton('test_feature_alpha').click(); - await expect(featureSettings.copyToModalApply).toBeVisible(); - await expect(featureSettings.copyToModalApply).toBeDisabled(); - }); - - await test.step('select target sub-feature', async () => { - await featureSettings.copyToModalCheckbox('test_feature_beta').click(); - }); - - await test.step('apply copies endpoints to target', async () => { - await expect(featureSettings.copyToModalApply).toBeEnabled(); - await featureSettings.copyToModalApply.click(); - await expect(featureSettings.copyToModalApply).toBeHidden(); - }); - - await test.step('target sub-feature now contains source endpoint', async () => { - await expect(betaCard).toContainText('anthropic'); - }); - - await test.step('save button becomes enabled', async () => { - await expect(featureSettings.saveButton).toBeEnabled(); - }); - } finally { - await featureSettings.unmockInferenceEndpoints(); - } + const betaCard = featureSettings.subFeatureCard('test_feature_beta'); + + await test.step('target sub-feature shows its original endpoint', async () => { + await expect(betaCard).toContainText('openai'); + await expect(betaCard).not.toContainText('anthropic'); + }); + + await test.step('open copy to modal from source sub-feature', async () => { + await featureSettings.copyToButton('test_feature_alpha').click(); + await expect(featureSettings.copyToModalApply).toBeVisible(); + await expect(featureSettings.copyToModalApply).toBeDisabled(); + }); + + await test.step('select target sub-feature', async () => { + await featureSettings.copyToModalCheckbox('test_feature_beta').click(); + }); + + await test.step('apply copies endpoints to target', async () => { + await expect(featureSettings.copyToModalApply).toBeEnabled(); + await featureSettings.copyToModalApply.click(); + await expect(featureSettings.copyToModalApply).toBeHidden(); + }); + + await test.step('target sub-feature now contains source endpoint', async () => { + await expect(betaCard).toContainText('anthropic'); + }); + + await test.step('save button becomes enabled', async () => { + await expect(featureSettings.saveButton).toBeEnabled(); + }); }); } );