diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_map.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_map.ts index a6b32bbeeeb35..ca4103a6c4159 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_map.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/page_objects/service_map.ts @@ -27,6 +27,14 @@ export class ServiceMapPage { public serviceMapFocusMapButton: Locator; public serviceMapDependencyDetailsButton: Locator; public serviceMapEdgeExploreTracesButton: Locator; + public serviceMapOptionsPanel: Locator; + public serviceMapFindInPageInput: Locator; + /** + * Native search `` (`SERVICE_MAP_FIND_INPUT_ID`). Prefer this for fill/focus so React + * `onFocus` runs and find highlights sync (`service_map_find_in_page` gates on `isFocused`). + */ + public serviceMapFindInPageNativeInput: Locator; + public serviceMapFindMatchSummary: Locator; constructor(private readonly page: ScoutPage, private readonly kbnUrl: KibanaUrl) { this.serviceMap = page.testSubj.locator('serviceMap'); @@ -52,6 +60,10 @@ export class ServiceMapPage { this.serviceMapEdgeExploreTracesButton = page.testSubj.locator( 'apmEdgeContentsOpenInDiscoverButton' ); + this.serviceMapOptionsPanel = page.testSubj.locator('serviceMapOptionsPanel'); + this.serviceMapFindInPageInput = page.testSubj.locator('serviceMapControlsSearch'); + this.serviceMapFindInPageNativeInput = page.locator('#serviceMapFindInPageInput'); + this.serviceMapFindMatchSummary = page.testSubj.locator('serviceMapFindMatchSummary'); } async gotoWithDateSelected(start: string, end: string, options?: { kuery?: string }) { @@ -95,6 +107,23 @@ export class ServiceMapPage { await this.serviceMapGraph.waitFor({ state: 'visible' }); } + /** + * Blur focused controls and move focus to `document.body` so the service map Ctrl/Cmd+K handler + * treats the shortcut as in scope (see graph.tsx). + */ + async focusBodyForMapShortcuts() { + await this.page.evaluate(() => { + (document.activeElement as HTMLElement | null)?.blur?.(); + document.body.focus(); + }); + } + + /** Triggers find-in-page focus via the same shortcut as the in-app hint (Control+K / Meta+K). */ + async openFindInPageWithKeyboardShortcut() { + await this.focusBodyForMapShortcuts(); + await this.page.keyboard.press('Control+KeyK'); + } + async clickZoom(direction: 'in' | 'out') { const button = direction === 'in' ? this.zoomInBtn : this.zoomOutBtn; await button.waitFor({ state: 'visible' }); @@ -183,6 +212,15 @@ export class ServiceMapPage { return this.serviceMapGraph.getByTestId(`serviceMapNode-service-${serviceName}`); } + /** + * Highlight frame around the active find-in-page match (`HighlightWrapper` when `isActiveSearchMatch`). + */ + getActiveFindMatchHighlightFrame(serviceName: string) { + return this.getServiceNodeRoot(serviceName).locator( + 'xpath=ancestor::*[@data-test-subj="serviceMapNodeSearchHighlightFrame"][1]' + ); + } + /** * The clickable/focusable service circle only. Prefer this over role+name: when shown, violated/degrading SLO * badges can also be buttons whose accessible name includes the service name, so `getByRole('button', { name })` diff --git a/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_map/service_map_a11y.spec.ts b/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_map/service_map_a11y.spec.ts index 7bf3ee32aa411..933d923d527fd 100644 --- a/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_map/service_map_a11y.spec.ts +++ b/x-pack/solutions/observability/plugins/apm/test/scout/ui/parallel_tests/service_map/service_map_a11y.spec.ts @@ -49,6 +49,26 @@ test.describe( await expect(node).toBeFocused(); }); + await test.step('find-in-page: highlight frame while focused, Enter centers match', async () => { + await serviceMapPage.focusBodyForMapShortcuts(); + await serviceMapPage.openFindInPageWithKeyboardShortcut(); + // Fill the real input (#serviceMapFindInPageInput) so EuiFieldSearch onFocus runs and + // highlight context updates (filling by layout test-subj alone can leave isFocused false). + await serviceMapPage.serviceMapFindInPageNativeInput.fill(SERVICE_OPBEANS_JAVA); + await expect(serviceMapPage.serviceMapFindMatchSummary).toHaveText(/[1-9]/); + + // Highlights are driven only while the find field is focused; centering the map after Enter + // can move focus and clear highlights, so assert the frame before Enter. + const highlightFrame = + serviceMapPage.getActiveFindMatchHighlightFrame(SERVICE_OPBEANS_JAVA); + await expect(highlightFrame).toBeVisible(); + await expect(highlightFrame).toHaveAttribute('data-search-active-match'); + + await serviceMapPage.serviceMapFindInPageNativeInput.press('Enter'); + await serviceMapPage.settleServiceMapLayout(); + await expect(serviceMapPage.serviceMapFindMatchSummary).toHaveText(/[1-9]/); + }); + await test.step('zoom controls are keyboard accessible', async () => { await serviceMapPage.clickFitView(); await expect(serviceMapPage.zoomInBtnControl).toBeVisible();