Skip to content

Commit 6af6d36

Browse files
authored
[SharedUX] Migrate content management example plugin FTRs to Scout (elastic#268687)
Relates to elastic/kibana-team#2818 ## Summary - Migrated the Content Management example plugin FTR tests to Scout as part of SharedUX's OnWeek initiative to migrate all SharedUX owned FTR tests to Scout, improving CI costs and execution time. - Migrated 3 FTR tests from `src/platform/test/examples/content_management/` to Scout at `examples/content_management_examples/test/scout_examples/ui/` - Removed the FTR test files and their `loadTestFile` entry from `src/platform/test/examples/config.js` - Added `contentManagementExamples` to `.buildkite/scout_ci_config.yml` for CI discovery - Updated `tsconfig.json` and regenerated `moon.yml` to include Scout test paths and `@kbn/scout` dependency - Added a shared `ListingTable` page object to `@kbn/scout` core page objects (not essential for these tests but foundation for upcoming [Saved Object Tagging migration](elastic/kibana-team#2817)) - Used the `scout-migrate-from-ftr` skill to plan and execute the migration, and `scout-best-practices-reviewer` to validate parity and best practices compliance. ### Validation - Tests pass locally against Scout `examples` server config set - Ran `--repeat-each 30` to verify no flakiness <img width="955" height="486" alt="Screenshot 2026-05-11 at 15 56 08" src="https://github.com/user-attachments/assets/e59e1f76-a7cb-41c8-af2a-b70694f1c32e" />
1 parent e82430c commit 6af6d36

17 files changed

Lines changed: 213 additions & 252 deletions

File tree

.buildkite/scout_ci_config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ plugins:
1010
- banners
1111
- cloud_security_posture
1212
- console
13+
- contentManagementExamples
1314
- custom_branding
1415
- dashboard
1516
- dashboard_markdown

examples/content_management_examples/moon.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependsOn:
3030
- '@kbn/saved-objects-finder-plugin'
3131
- '@kbn/content-management-table-list-view-common'
3232
- '@kbn/core-saved-objects-api-server'
33+
- '@kbn/scout'
3334
tags:
3435
- plugin
3536
- prod
@@ -43,6 +44,7 @@ fileGroups:
4344
- public/**/*.ts
4445
- public/**/*.tsx
4546
- server/**/*.ts
47+
- test/scout_examples/**/*
4648
- '!target/**/*'
4749
jest-config:
4850
- jest.config.js

src/platform/test/examples/content_management/index.ts renamed to examples/content_management_examples/test/scout_examples/ui/playwright.config.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,8 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import type { PluginFunctionalProviderContext } from '../../plugin_functional/services';
10+
import { createPlaywrightConfig } from '@kbn/scout';
1111

12-
// eslint-disable-next-line import/no-default-export
13-
export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
14-
describe('content management examples', function () {
15-
loadTestFile(require.resolve('./todo_app'));
16-
loadTestFile(require.resolve('./msearch'));
17-
loadTestFile(require.resolve('./finder'));
18-
});
19-
}
12+
export default createPlaywrightConfig({
13+
testDir: './tests',
14+
});
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { test } from '@kbn/scout';
11+
import { expect } from '@kbn/scout/ui';
12+
13+
test.describe('Content Management Examples', { tag: ['@local-stateful-classic'] }, () => {
14+
test.beforeAll(async ({ apiServices }) => {
15+
await apiServices.sampleData.install('flights');
16+
});
17+
18+
test.afterAll(async ({ apiServices }) => {
19+
await apiServices.sampleData.remove('flights');
20+
});
21+
22+
test('Todo app CRUD operations work', async ({ browserAuth, page }) => {
23+
await browserAuth.loginAsViewer();
24+
await page.gotoApp('contentManagementExamples');
25+
await expect(page.testSubj.locator('todosExample')).toBeVisible();
26+
27+
const todoItems = page.locator('[data-test-subj~="todoItem"]');
28+
29+
await expect(todoItems).toHaveCount(2);
30+
31+
// Filter by "Completed" — expect 1 item
32+
await page.getByRole('button', { name: 'Completed' }).click();
33+
await expect(page.testSubj.locator('todoPending')).toHaveCount(0);
34+
await expect(todoItems).toHaveCount(1);
35+
36+
// Filter by "Todo" — expect 1 item
37+
await page.getByRole('button', { name: 'Todo' }).click();
38+
await expect(page.testSubj.locator('todoPending')).toHaveCount(0);
39+
await expect(todoItems).toHaveCount(1);
40+
41+
// Filter by "All" — expect 2 items
42+
await page.getByRole('button', { name: 'All' }).click();
43+
await expect(page.testSubj.locator('todoPending')).toHaveCount(0);
44+
await expect(todoItems).toHaveCount(2);
45+
46+
// Add a new todo
47+
const newTodoInput = page.testSubj.locator('newTodo');
48+
await newTodoInput.fill('New todo');
49+
await newTodoInput.press('Enter');
50+
await expect(page.testSubj.locator('todoPending')).toHaveCount(0);
51+
await expect(todoItems).toHaveCount(3);
52+
53+
// Verify new todo text and checkbox state
54+
const newTodo = todoItems.filter({ hasText: 'New todo' });
55+
await expect(newTodo).toBeVisible();
56+
const newTodoCheckbox = newTodo.locator('[data-test-subj~="todoCheckbox"]');
57+
await expect(newTodoCheckbox).not.toBeChecked();
58+
59+
// Toggle the new todo to completed
60+
await newTodo.locator('label').click();
61+
await expect(page.testSubj.locator('todoPending')).toHaveCount(0);
62+
63+
// Verify it appears under "Completed" filter
64+
await page.getByRole('button', { name: 'Completed' }).click();
65+
await expect(page.testSubj.locator('todoPending')).toHaveCount(0);
66+
await expect(todoItems).toHaveCount(2);
67+
const completedNewTodo = todoItems.filter({ hasText: 'New todo' });
68+
await expect(completedNewTodo).toBeVisible();
69+
const completedCheckbox = completedNewTodo.locator('[data-test-subj~="todoCheckbox"]');
70+
await expect(completedCheckbox).toBeChecked();
71+
72+
// Delete the new todo
73+
await completedNewTodo.getByLabel('Delete').click();
74+
await expect(page.testSubj.locator('todoPending')).toHaveCount(0);
75+
await expect(todoItems).toHaveCount(1);
76+
});
77+
78+
test('MSearch demo displays sample flights data', async ({
79+
browserAuth,
80+
page,
81+
pageObjects,
82+
kbnUrl,
83+
}) => {
84+
await browserAuth.loginAsViewer();
85+
await page.goto(kbnUrl.get('/app/contentManagementExamples/msearch'));
86+
87+
await expect(page.testSubj.locator('msearchExample')).toBeVisible();
88+
await pageObjects.listingTable.waitUntilTableIsLoaded();
89+
90+
const expectedItems = [
91+
'kibana_sample_data_flights',
92+
'[Flights] Airport Connections (Hover Over Airport)',
93+
'[Flights] Departures Count Map',
94+
'[Flights] Origin Time Delayed',
95+
'[Flights] Flight Log',
96+
];
97+
98+
await expect
99+
.poll(() => pageObjects.listingTable.getAllItemsNames())
100+
.toEqual(expect.arrayContaining(expectedItems));
101+
});
102+
103+
test('Finder demo displays saved objects from sample data', async ({
104+
browserAuth,
105+
page,
106+
kbnUrl,
107+
}) => {
108+
await browserAuth.loginAsViewer();
109+
await page.goto(kbnUrl.get('/app/contentManagementExamples/finder'));
110+
111+
await expect(page.testSubj.locator('finderExample')).toBeVisible();
112+
await expect(page.testSubj.locator('savedObjectsFinderTable')).toBeVisible();
113+
114+
const titleElements = page.testSubj.locator('savedObjectFinderTitle');
115+
await expect(titleElements.first()).toBeVisible();
116+
117+
const titles = await titleElements.locator('.euiLink').allTextContents();
118+
119+
const expectedItems = [
120+
'Kibana Sample Data Flights',
121+
'[Flights] Airport Connections (Hover Over Airport)',
122+
'[Flights] Departures Count Map',
123+
'[Flights] Origin Time Delayed',
124+
'[Flights] Flight Log',
125+
];
126+
127+
for (const item of expectedItems) {
128+
expect(titles).toContain(item);
129+
}
130+
});
131+
});

examples/content_management_examples/tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"public/**/*.ts",
1010
"public/**/*.tsx",
1111
"server/**/*.ts",
12+
"test/scout_examples/**/*",
1213
"../../typings/**/*"
1314
],
1415
"exclude": [
@@ -28,5 +29,6 @@
2829
"@kbn/saved-objects-finder-plugin",
2930
"@kbn/content-management-table-list-view-common",
3031
"@kbn/core-saved-objects-api-server",
32+
"@kbn/scout"
3133
]
3234
}

src/platform/packages/shared/content-management/table_list_view_table/src/__jest__/created_by_column.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe('created_by column', () => {
112112
render(<TableListView {...requiredProps} />);
113113

114114
// wait until first render
115-
expect(await screen.findByTestId('itemsInMemTable')).toBeVisible();
115+
expect(await screen.findByTestId('listingTable-isLoaded')).toBeVisible();
116116

117117
expect(() => screen.getByTestId(/tableHeaderCell_createdBy/)).toThrow();
118118
});
@@ -130,7 +130,7 @@ describe('created_by column', () => {
130130
);
131131

132132
// wait until first render
133-
expect(await screen.findByTestId('itemsInMemTable')).toBeVisible();
133+
expect(await screen.findByTestId('listingTable-isLoaded')).toBeVisible();
134134

135135
expect(() => screen.getByTestId(/tableHeaderCell_createdBy/)).toThrow();
136136
});

src/platform/packages/shared/content-management/table_list_view_table/src/__jest__/created_by_filter.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ describe('created_by filter', () => {
140140
test('filtering by one creator shows correct item count', async () => {
141141
render(<TableListView {...requiredProps} createdByEnabled={true} />);
142142

143-
expect(await screen.findByTestId('itemsInMemTable')).toBeVisible();
143+
expect(await screen.findByTestId('listingTable-isLoaded')).toBeVisible();
144144

145145
await user.click(screen.getByTestId('userFilterPopoverButton'));
146146

@@ -155,7 +155,7 @@ describe('created_by filter', () => {
155155
test('filtering by multiple creators shows correct item count', async () => {
156156
render(<TableListView {...requiredProps} createdByEnabled={true} />);
157157

158-
expect(await screen.findByTestId('itemsInMemTable')).toBeVisible();
158+
expect(await screen.findByTestId('listingTable-isLoaded')).toBeVisible();
159159

160160
await user.click(screen.getByTestId('userFilterPopoverButton'));
161161

src/platform/packages/shared/content-management/table_list_view_table/src/components/table.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ export function Table<T extends UserContentCommonSchema>({
396396
executeQueryOptions={{ enabled: false }}
397397
sorting={sorting}
398398
onChange={onTableChange}
399-
data-test-subj="itemsInMemTable"
399+
data-test-subj={isFetchingItems ? 'listingTable-isLoading' : 'listingTable-isLoaded'}
400400
rowHeader="attributes.title"
401401
tableCaption={tableCaption}
402402
scrollableInline

0 commit comments

Comments
 (0)