Skip to content

Commit b654604

Browse files
rmyzdmlemeshkokibanamachine
authored andcommitted
[Observability][Scout] Migrate navigation e2e tests to Scout (elastic#264422)
Co-authored-by: Dzmitry Lemechko <dzmitry.lemechko@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent 5d1b930 commit b654604

29 files changed

Lines changed: 942 additions & 578 deletions

File tree

.buildkite/ftr_oblt_stateful_configs.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,3 @@ enabled:
4949
- x-pack/solutions/observability/test/api_integration_deployment_agnostic/configs/stateful/oblt.ai_assistant.stateful.config.ts
5050
- x-pack/solutions/observability/test/api_integration_deployment_agnostic/feature_flag_configs/stateful/oblt.stateful.config.ts
5151
- x-pack/solutions/observability/test/api_integration_deployment_agnostic/configs/stateful/oblt.ai_agent.stateful.config.ts
52-
- x-pack/solutions/observability/test/functional_solution_sidenav/config.ts

.buildkite/scout_ci_config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ plugins:
4040
- search_inference_endpoints
4141
- security
4242
- security_solution
43+
- serverless_observability
4344
- share
4445
- slo
4546
- spaces

.github/CODEOWNERS

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1889,7 +1889,6 @@ x-pack/platform/plugins/shared/streams/test/scout/api/tests/sig_events @elastic/
18891889

18901890
# Observability-ui
18911891
/x-pack/solutions/observability/test/serverless/api_integration/configs/index.ts @elastic/observability-ui
1892-
/x-pack/solutions/observability/test/functional_solution_sidenav/ @elastic/observability-ui
18931892
/x-pack/solutions/observability/test/functional/page_objects/observability_page.ts @elastic/observability-ui
18941893
/x-pack/solutions/observability/plugins/observability/server/services/index.ts @elastic/observability-ui
18951894

x-pack/solutions/observability/packages/kbn-scout-oblt/src/playwright/fixtures/parallel_run_fixtures.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
* 2.0.
66
*/
77

8-
import { spaceTest as spaceBase, mergeTests } from '@kbn/scout';
8+
import { spaceTest as spaceBase } from '@kbn/scout';
99
import { extendPageObjects } from '../page_objects';
10-
import { profilingSetupFixture } from './worker';
1110

1211
import type { ObltParallelTestFixtures, ObltParallelWorkerFixtures } from './types';
1312

14-
const baseFixture = spaceBase.extend<ObltParallelTestFixtures, ObltParallelWorkerFixtures>({
13+
/**
14+
* Does not merge `profilingSetupFixture`: it extends non-space `test`; `mergeTests`
15+
* would overwrite the space-scoped `page` and break `scoutSpace` isolation.
16+
* Profiling stays on single-thread `test` / `apiTest` / global setup.
17+
*/
18+
export const spaceTest = spaceBase.extend<ObltParallelTestFixtures, ObltParallelWorkerFixtures>({
1519
pageObjects: async (
1620
{
1721
pageObjects,
@@ -34,8 +38,3 @@ const baseFixture = spaceBase.extend<ObltParallelTestFixtures, ObltParallelWorke
3438
{ scope: 'worker' },
3539
],
3640
});
37-
38-
/**
39-
* Should be used test spec files, running in parallel in isolated spaces against the same Kibana instance.
40-
*/
41-
export const spaceTest = mergeTests(baseFixture, profilingSetupFixture);

x-pack/solutions/observability/packages/kbn-scout-oblt/src/playwright/fixtures/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,4 @@ export interface ObltParallelTestFixtures extends ScoutParallelTestFixtures {
3333

3434
export interface ObltParallelWorkerFixtures extends ScoutParallelWorkerFixtures {
3535
apiServices: ObltApiServicesFixture;
36-
profilingSetup: ProfilingSetupFixture;
3736
}

x-pack/solutions/observability/packages/kbn-scout-oblt/src/playwright/page_objects/observability_navigation.ts

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,153 @@
55
* 2.0.
66
*/
77

8-
import type { ScoutPage } from '@kbn/scout';
8+
import type { Locator, ScoutPage } from '@kbn/scout';
99

10+
/** Chrome nav for Observability — locators and actions only; specs own `expect`. */
1011
export class ObservabilityNavigation {
11-
constructor(private readonly page: ScoutPage) {}
12+
public readonly sidenav: Locator;
13+
public readonly primaryNav: Locator;
14+
public readonly footerNav: Locator;
15+
public readonly morePopover: Locator;
16+
public readonly breadcrumbs: Locator;
17+
public readonly logo: Locator;
18+
public readonly moreMenuTrigger: Locator;
1219

20+
constructor(private readonly page: ScoutPage) {
21+
this.sidenav = this.page.testSubj.locator('kbnChromeLayoutNavigation');
22+
this.primaryNav = this.page.testSubj.locator('kbnChromeNav-primaryNavigation');
23+
this.footerNav = this.page.testSubj.locator('kbnChromeNav-footer');
24+
this.morePopover = this.page.testSubj.locator('side-nav-popover-More');
25+
this.breadcrumbs = this.page.testSubj.locator('breadcrumbs');
26+
this.logo = this.page.testSubj.locator('nav-header-logo');
27+
this.moreMenuTrigger = this.page.testSubj.locator('kbnChromeNav-moreMenuTrigger');
28+
}
29+
30+
/** `goto*` does not call `waitForLoad()` — sidenav may be absent (e.g. classic chrome); call `waitForLoad()` when interacting with nav. */
1331
async goto() {
1432
await this.page.gotoApp('observability');
1533
}
1634

1735
async gotoLanding() {
1836
await this.page.gotoApp('observability/landing');
1937
}
38+
39+
async gotoApp(appName: string) {
40+
await this.page.gotoApp(appName);
41+
}
42+
43+
/** Waits on `primaryNav` (outer layout can be 0-width until CSS vars apply). */
44+
async waitForLoad() {
45+
await this.primaryNav.waitFor({ state: 'visible' });
46+
}
47+
48+
/** App root or `kbnNoDataPage` (Discover/Dashboards with no data views). */
49+
pageOrNoData(testSubj: string): Locator {
50+
return this.page.testSubj.locator(testSubj).or(this.page.testSubj.locator('kbnNoDataPage'));
51+
}
52+
53+
navItemInPrimaryByDeepLinkId(deepLinkId: string): Locator {
54+
return this.primaryNav.locator(`[data-test-subj~="nav-item-deepLinkId-${deepLinkId}"]`);
55+
}
56+
57+
navItemInPrimaryById(id: string): Locator {
58+
return this.primaryNav.locator(`[data-test-subj~="nav-item-id-${id}"]`);
59+
}
60+
61+
/** Primary or More — for overflow-dependent placement; prefer scoped helpers when fixed. */
62+
navItemInBodyByDeepLinkId(deepLinkId: string): Locator {
63+
const selector = `[data-test-subj~="nav-item-deepLinkId-${deepLinkId}"]`;
64+
return this.primaryNav.locator(selector).or(this.morePopover.locator(selector));
65+
}
66+
67+
navItemInBodyById(id: string): Locator {
68+
const selector = `[data-test-subj~="nav-item-id-${id}"]`;
69+
return this.primaryNav.locator(selector).or(this.morePopover.locator(selector));
70+
}
71+
72+
navItemInFooterByDeepLinkId(deepLinkId: string): Locator {
73+
return this.footerNav.locator(`[data-test-subj~="nav-item-deepLinkId-${deepLinkId}"]`);
74+
}
75+
76+
navItemInFooterById(id: string): Locator {
77+
return this.footerNav.locator(`[data-test-subj~="nav-item-id-${id}"]`);
78+
}
79+
80+
navItemInMoreByDeepLinkId(deepLinkId: string): Locator {
81+
return this.morePopover.locator(`[data-test-subj~="nav-item-deepLinkId-${deepLinkId}"]`);
82+
}
83+
84+
navItemInMoreById(id: string): Locator {
85+
return this.morePopover.locator(`[data-test-subj~="nav-item-id-${id}"]`);
86+
}
87+
88+
navItemInSidenavByDeepLinkId(deepLinkId: string): Locator {
89+
return this.sidenav.locator(`[data-test-subj~="nav-item-deepLinkId-${deepLinkId}"]`);
90+
}
91+
92+
navItemInSidenavById(id: string): Locator {
93+
return this.sidenav.locator(`[data-test-subj~="nav-item-id-${id}"]`);
94+
}
95+
96+
/** Item with `nav-item-isActive` in test-subj (current route). */
97+
activeNavItemByDeepLinkId(deepLinkId: string): Locator {
98+
return this.sidenav.locator(
99+
`[data-test-subj~="nav-item-deepLinkId-${deepLinkId}"][data-test-subj~="nav-item-isActive"]`
100+
);
101+
}
102+
103+
activeNavItemById(id: string): Locator {
104+
return this.sidenav.locator(
105+
`[data-test-subj~="nav-item-id-${id}"][data-test-subj~="nav-item-isActive"]`
106+
);
107+
}
108+
109+
sidePanel(id: string): Locator {
110+
return this.page.testSubj.locator(`~kbnChromeNav-sidePanel_${id}`);
111+
}
112+
113+
nestedPanel(id: string): Locator {
114+
return this.morePopover.locator(`[data-test-subj="kbnChromeNav-nestedPanel-${id}"]`);
115+
}
116+
117+
anyPanel(id: string): Locator {
118+
return this.sidePanel(id).or(this.nestedPanel(id));
119+
}
120+
121+
/** By `breadcrumb-deepLinkId-*` test-subj or visible text. */
122+
breadcrumb(by: { deepLinkId: string } | { text: string }): Locator {
123+
if ('deepLinkId' in by) {
124+
return this.breadcrumbs.locator(`[data-test-subj~="breadcrumb-deepLinkId-${by.deepLinkId}"]`);
125+
}
126+
return this.breadcrumbs.locator('[data-test-subj~="breadcrumb"]', { hasText: by.text });
127+
}
128+
129+
/** If More is already open, Escape first so the next open is the root list. */
130+
async openMoreMenu() {
131+
if (await this.morePopover.isVisible()) {
132+
await this.page.keyboard.press('Escape');
133+
await this.morePopover.waitFor({ state: 'hidden' });
134+
}
135+
await this.moreMenuTrigger.click();
136+
await this.morePopover.waitFor({ state: 'visible' });
137+
}
138+
139+
async clickLogo() {
140+
await this.logo.click();
141+
}
142+
143+
/** Returns a function that is false after a full page reload (spec asserts). */
144+
async createNoPageReloadCheck(): Promise<() => Promise<boolean>> {
145+
const trackReloadTs = Date.now();
146+
await this.page.evaluate((ts: number) => {
147+
(window as unknown as { __testTrackReload__?: number }).__testTrackReload__ = ts;
148+
}, trackReloadTs);
149+
150+
return async () => {
151+
return this.page.evaluate((ts: number) => {
152+
const w = window as unknown as { __testTrackReload__?: number };
153+
return w.__testTrackReload__ === ts;
154+
}, trackReloadTs);
155+
};
156+
}
20157
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { spaceTest as test, tags } from '@kbn/scout-oblt';
9+
import { expect } from '@kbn/scout-oblt/ui';
10+
11+
test.describe(
12+
'Stateful Observability Navigation - Footer',
13+
{ tag: [...tags.stateful.observability] },
14+
() => {
15+
test.beforeAll(async ({ scoutSpace }) => {
16+
await scoutSpace.setSolutionView('oblt');
17+
});
18+
19+
test.beforeEach(async ({ browserAuth, pageObjects }) => {
20+
await browserAuth.loginAsAdmin();
21+
await pageObjects.observabilityNavigation.goto();
22+
await pageObjects.observabilityNavigation.waitForLoad();
23+
});
24+
25+
test('renders expected footer items with working links', async ({ pageObjects }) => {
26+
const nav = pageObjects.observabilityNavigation;
27+
28+
await test.step('Add data or Ingest Hub entry is visible and linked', async () => {
29+
const addData = nav.navItemInFooterByDeepLinkId('observabilityOnboarding');
30+
const ingestHub = nav.navItemInFooterByDeepLinkId('ingestHub');
31+
const item = addData.or(ingestHub);
32+
await expect(item).toBeVisible();
33+
await expect(item).toHaveAttribute('href', /.+/);
34+
});
35+
36+
await test.step('Developer tools is visible and linked', async () => {
37+
const item = nav.navItemInFooterById('devTools');
38+
await expect(item).toBeVisible();
39+
await expect(item).toHaveAttribute('href', /.+/);
40+
});
41+
42+
await test.step('Data management panel opener is visible', async () => {
43+
await expect(nav.navItemInFooterById('data_management')).toBeVisible();
44+
});
45+
46+
await test.step('Stack Management panel opener is visible', async () => {
47+
await expect(nav.navItemInFooterById('stack_management')).toBeVisible();
48+
});
49+
});
50+
51+
test('clicking footer items navigates to the correct destinations', async ({
52+
pageObjects,
53+
page,
54+
}) => {
55+
const nav = pageObjects.observabilityNavigation;
56+
57+
await test.step('Developer tools navigates to the console app', async () => {
58+
await nav.navItemInFooterById('devTools').click();
59+
await expect(page.testSubj.locator('console')).toBeVisible();
60+
});
61+
62+
await test.step('Data management opens its side panel', async () => {
63+
await nav.navItemInFooterById('data_management').click();
64+
await expect(nav.sidePanel('data_management')).toBeVisible();
65+
});
66+
67+
await test.step('Stack Management opens its side panel', async () => {
68+
await nav.navItemInFooterById('stack_management').click();
69+
await expect(nav.sidePanel('stack_management')).toBeVisible();
70+
});
71+
});
72+
73+
test('Stack Management panel children navigate and update breadcrumbs', async ({
74+
pageObjects,
75+
}) => {
76+
const nav = pageObjects.observabilityNavigation;
77+
78+
await test.step('stack_management → Tags', async () => {
79+
await nav.navItemInFooterById('stack_management').click();
80+
await expect(nav.sidePanel('stack_management')).toBeVisible();
81+
82+
await nav
83+
.sidePanel('stack_management')
84+
.locator('[data-test-subj~="nav-item-id-management:tags"]')
85+
.click();
86+
await expect(nav.breadcrumb({ text: 'Tags' })).toBeVisible();
87+
});
88+
89+
await test.step('stack_management → Maintenance Windows', async () => {
90+
await nav.navItemInFooterById('stack_management').click();
91+
await expect(nav.sidePanel('stack_management')).toBeVisible();
92+
93+
await nav
94+
.sidePanel('stack_management')
95+
.locator('[data-test-subj~="nav-item-id-management:maintenanceWindows"]')
96+
.click();
97+
await expect(nav.breadcrumb({ text: 'Maintenance Windows' })).toBeVisible();
98+
});
99+
});
100+
101+
test('legacy management landing page opens the Stack Management panel', async ({
102+
pageObjects,
103+
page,
104+
}) => {
105+
const nav = pageObjects.observabilityNavigation;
106+
await page.gotoApp('management');
107+
await nav.waitForLoad();
108+
await expect(page.testSubj.locator('managementHomeSolution')).toBeVisible();
109+
await expect(nav.sidePanel('stack_management')).toBeVisible();
110+
});
111+
112+
test('active sidenav panel is re-opened after a browser refresh', async ({
113+
pageObjects,
114+
page,
115+
}) => {
116+
const nav = pageObjects.observabilityNavigation;
117+
118+
await nav.navItemInFooterById('stack_management').click();
119+
await expect(nav.sidePanel('stack_management')).toBeVisible();
120+
121+
await nav
122+
.sidePanel('stack_management')
123+
.locator('[data-test-subj~="nav-item-id-management:tags"]')
124+
.click();
125+
await expect(nav.breadcrumb({ text: 'Tags' })).toBeVisible();
126+
127+
await page.reload();
128+
await nav.waitForLoad();
129+
130+
await expect(nav.sidePanel('stack_management')).toBeVisible();
131+
});
132+
}
133+
);

0 commit comments

Comments
 (0)