Skip to content

Commit ce79eb2

Browse files
committed
chore: extract EuiTableTestHarness and migrate CCR tests to use it
1 parent 38e1f10 commit ce79eb2

7 files changed

Lines changed: 118 additions & 114 deletions

File tree

src/platform/packages/private/kbn-test-eui-helpers/src/rtl_helpers.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,77 @@ export class EuiSelectTestHarness {
196196
fireEvent.change(this.#selectEl, { target: { value: option } });
197197
}
198198
}
199+
200+
export class EuiTableTestHarness {
201+
#testId: string;
202+
203+
/**
204+
* Returns table element or throws
205+
*/
206+
get #tableEl() {
207+
return screen.getByTestId(this.#testId);
208+
}
209+
210+
constructor(testId: string) {
211+
this.#testId = testId;
212+
}
213+
214+
/**
215+
* Returns `data-test-subj` of table
216+
*/
217+
public get testId() {
218+
return this.#testId;
219+
}
220+
221+
/**
222+
* Returns table if found, otherwise `null`
223+
*/
224+
public get self() {
225+
return screen.queryByTestId(this.#testId);
226+
}
227+
228+
public get body() {
229+
return this.#tableEl?.querySelector('tbody');
230+
}
231+
232+
/**
233+
* Get table row elements for direct interaction with cells.
234+
* Useful when you need to click buttons, checkboxes, or other interactive elements within rows.
235+
*
236+
* @returns {HTMLElement[]} Array of table row elements
237+
*
238+
* @example
239+
* const table = new EuiTableTestHarness('myTable');
240+
* const rows = table.rows;
241+
* const firstRowCheckbox = within(rows[0]).getByRole('checkbox');
242+
* await user.click(firstRowCheckbox);
243+
*/
244+
public get rows(): HTMLElement[] {
245+
return Array.from(this.body?.querySelectorAll('tr') ?? []);
246+
}
247+
248+
/**
249+
* Get table cell values as a 2D array.
250+
* Useful for asserting table data in a structured way.
251+
*
252+
* @returns {string[][]} Array of rows, where each row is an array of cell text values
253+
*
254+
* @example
255+
* const table = new EuiTableTestHarness('myTable');
256+
* const cellValues = table.getCellValues();
257+
* expect(cellValues[0]).toEqual(['', 'Name', 'Status', '']);
258+
*/
259+
public get cellValues(): string[][] {
260+
return this.rows.map((row) => {
261+
const cells = Array.from(row.querySelectorAll('td'));
262+
return cells.map((cell) => {
263+
const content =
264+
cell.querySelector('.euiTableCellContent__text') ||
265+
cell.querySelector('.euiTableCellContent') ||
266+
cell;
267+
// Preserve original text content including whitespace (don't trim)
268+
return content.textContent || '';
269+
});
270+
});
271+
}
272+
}

x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { screen, within, act } from '@testing-library/react';
99
import './mocks';
1010
import { getAutoFollowPatternMock } from './fixtures/auto_follow_pattern';
1111
import { setupEnvironment, pageHelpers, getRandomString } from './helpers';
12-
import { getTableCellsValues, getTableRows } from './helpers/eui_table';
12+
import { EuiTableTestHarness } from '@kbn/test-eui-helpers';
1313

1414
const { setup } = pageHelpers.autoFollowPatternList;
1515

@@ -92,7 +92,8 @@ describe('<AutoFollowPatternList />', () => {
9292
test('pagination works', async () => {
9393
await actions.clickPaginationNextButton();
9494

95-
const tableCellsValues = getTableCellsValues('autoFollowPatternListTable');
95+
const table = new EuiTableTestHarness('autoFollowPatternListTable');
96+
const tableCellsValues = table.cellValues;
9697
// Pagination defaults to 20 auto-follow patterns per page. We loaded 30 auto-follow patterns,
9798
// so the second page should have 10.
9899
expect(tableCellsValues.length).toBe(10);
@@ -101,7 +102,8 @@ describe('<AutoFollowPatternList />', () => {
101102
test('search works', async () => {
102103
await actions.search('unique');
103104

104-
const tableCellsValues = getTableCellsValues('autoFollowPatternListTable');
105+
const table = new EuiTableTestHarness('autoFollowPatternListTable');
106+
const tableCellsValues = table.cellValues;
105107
expect(tableCellsValues.length).toBe(1);
106108
});
107109
});
@@ -143,7 +145,8 @@ describe('<AutoFollowPatternList />', () => {
143145
});
144146

145147
test('should list the auto-follow patterns in the table', () => {
146-
const tableCellsValues = getTableCellsValues('autoFollowPatternListTable');
148+
const table = new EuiTableTestHarness('autoFollowPatternListTable');
149+
const tableCellsValues = table.cellValues;
147150
expect(tableCellsValues.length).toEqual(autoFollowPatterns.length);
148151
// Check key columns (status includes non-breaking space from EUI component)
149152
expect(tableCellsValues.length).toBe(2);
@@ -192,7 +195,8 @@ describe('<AutoFollowPatternList />', () => {
192195

193196
test('should remove the auto-follow pattern from the table after delete is complete', async () => {
194197
// Make sure that we have our 2 auto-follow patterns in the table
195-
let tableCellsValues = getTableCellsValues('autoFollowPatternListTable');
198+
const table = new EuiTableTestHarness('autoFollowPatternListTable');
199+
let tableCellsValues = table.cellValues;
196200
expect(tableCellsValues.length).toBe(2);
197201

198202
// We will delete the *first* auto-follow pattern in the table
@@ -208,16 +212,16 @@ describe('<AutoFollowPatternList />', () => {
208212
await actions.clickBulkDeleteButton();
209213
await actions.clickConfirmModalDeleteAutoFollowPattern();
210214

211-
tableCellsValues = getTableCellsValues('autoFollowPatternListTable');
215+
tableCellsValues = table.cellValues;
212216
expect(tableCellsValues.length).toBe(1);
213217
expect(tableCellsValues[0][1]).toEqual(autoFollowPattern2.name);
214218
});
215219
});
216220

217221
describe('table row actions', () => {
218222
test('should have a "pause", "delete" and "edit" action button on each row', async () => {
219-
const rows = getTableRows('autoFollowPatternListTable');
220-
const actionsCell = within(rows[0]).getAllByRole('cell').pop();
223+
const table = new EuiTableTestHarness('autoFollowPatternListTable');
224+
const actionsCell = within(table.rows[0]).getAllByRole('cell').pop();
221225
const contextMenuButton = within(actionsCell).getByRole('button');
222226

223227
await user.click(contextMenuButton);
@@ -230,8 +234,8 @@ describe('<AutoFollowPatternList />', () => {
230234
test('should open a confirmation modal when clicking on "delete" button', async () => {
231235
expect(screen.queryByTestId('deleteAutoFollowPatternConfirmation')).not.toBeInTheDocument();
232236

233-
const rows = getTableRows('autoFollowPatternListTable');
234-
const actionsCell = within(rows[0]).getAllByRole('cell').pop();
237+
const table = new EuiTableTestHarness('autoFollowPatternListTable');
238+
const actionsCell = within(table.rows[0]).getAllByRole('cell').pop();
235239
const contextMenuButton = within(actionsCell).getByRole('button');
236240
await user.click(contextMenuButton);
237241

x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { fireEvent, screen, within, act } from '@testing-library/react';
99
import './mocks';
1010
import { getFollowerIndexMock } from './fixtures/follower_index';
1111
import { setupEnvironment, pageHelpers, getRandomString } from './helpers';
12-
import { getTableCellsValues, getTableRows } from './helpers/eui_table';
12+
import { EuiTableTestHarness } from '@kbn/test-eui-helpers';
1313

1414
const { setup } = pageHelpers.followerIndexList;
1515

@@ -79,13 +79,14 @@ describe('<FollowerIndicesList />', () => {
7979
});
8080

8181
test('pagination works', async () => {
82-
const initialRows = getTableRows('followerIndexListTable');
82+
const table = new EuiTableTestHarness('followerIndexListTable');
83+
const initialRows = table.rows;
8384
expect(initialRows.length).toBe(20); // Default page size
8485

8586
const nextButton = screen.getByLabelText('Next page');
8687
fireEvent.click(nextButton);
8788

88-
const rowsAfterPagination = getTableRows('followerIndexListTable');
89+
const rowsAfterPagination = table.rows;
8990
expect(rowsAfterPagination.length).toBe(10); // Remaining items on page 2
9091
});
9192

@@ -94,12 +95,14 @@ describe('<FollowerIndicesList />', () => {
9495
fireEvent.change(searchBox, { target: { value: 'unique' } });
9596
fireEvent.blur(searchBox);
9697

97-
const rows = getTableRows('followerIndexListTable');
98+
const table = new EuiTableTestHarness('followerIndexListTable');
99+
const rows = table.rows;
98100
expect(rows.length).toBe(1);
99101
});
100102
});
101103

102104
describe('when there are follower indices', () => {
105+
let table;
103106
let tableCellsValues;
104107
let actions;
105108

@@ -116,7 +119,8 @@ describe('<FollowerIndicesList />', () => {
116119
await jest.runOnlyPendingTimersAsync();
117120
});
118121

119-
tableCellsValues = getTableCellsValues('followerIndexListTable');
122+
table = new EuiTableTestHarness('followerIndexListTable');
123+
tableCellsValues = table.cellValues;
120124
});
121125

122126
test('should not display the empty prompt', () => {
@@ -146,16 +150,14 @@ describe('<FollowerIndicesList />', () => {
146150
test('should be visible when a follower index is selected', async () => {
147151
expect(screen.queryByTestId('contextMenuButton')).not.toBeInTheDocument();
148152

149-
const rows = getTableRows('followerIndexListTable');
150-
const firstCheckbox = within(rows[0]).getByRole('checkbox');
153+
const firstCheckbox = within(table.rows[0]).getByRole('checkbox');
151154
await user.click(firstCheckbox);
152155

153156
expect(await screen.findByTestId('contextMenuButton')).toBeInTheDocument();
154157
});
155158

156159
test('should have a "pause", "edit" and "unfollow" action when the follower index is active', async () => {
157-
const rows = getTableRows('followerIndexListTable');
158-
const firstCheckbox = within(rows[0]).getByRole('checkbox');
160+
const firstCheckbox = within(table.rows[0]).getByRole('checkbox');
159161
await user.click(firstCheckbox);
160162

161163
const contextMenuButton = await screen.findByTestId('contextMenuButton');
@@ -173,8 +175,7 @@ describe('<FollowerIndicesList />', () => {
173175
});
174176

175177
test('should have a "resume", "edit" and "unfollow" action when the follower index is paused', async () => {
176-
const rows = getTableRows('followerIndexListTable');
177-
const secondCheckbox = within(rows[1]).getByRole('checkbox');
178+
const secondCheckbox = within(table.rows[1]).getByRole('checkbox');
178179
await user.click(secondCheckbox);
179180

180181
const contextMenuButton = await screen.findByTestId('contextMenuButton');
@@ -194,8 +195,7 @@ describe('<FollowerIndicesList />', () => {
194195
test('should open a confirmation modal when clicking on "pause replication"', async () => {
195196
expect(screen.queryByTestId('pauseReplicationConfirmation')).not.toBeInTheDocument();
196197

197-
const rows = getTableRows('followerIndexListTable');
198-
const firstCheckbox = within(rows[0]).getByRole('checkbox');
198+
const firstCheckbox = within(table.rows[0]).getByRole('checkbox');
199199
await user.click(firstCheckbox);
200200

201201
const contextMenuButton = await screen.findByTestId('contextMenuButton');
@@ -211,8 +211,7 @@ describe('<FollowerIndicesList />', () => {
211211
test('should open a confirmation modal when clicking on "unfollow leader index"', async () => {
212212
expect(screen.queryByTestId('unfollowLeaderConfirmation')).not.toBeInTheDocument();
213213

214-
const rows = getTableRows('followerIndexListTable');
215-
const firstCheckbox = within(rows[0]).getByRole('checkbox');
214+
const firstCheckbox = within(table.rows[0]).getByRole('checkbox');
216215
await user.click(firstCheckbox);
217216

218217
const contextMenuButton = await screen.findByTestId('contextMenuButton');
@@ -230,16 +229,14 @@ describe('<FollowerIndicesList />', () => {
230229
test('should open a context menu when clicking on the button of each row', async () => {
231230
expect(document.querySelector('.euiContextMenuPanel')).toBeNull();
232231

233-
const rows = getTableRows('followerIndexListTable');
234-
const actionButton = within(rows[0]).getByTestId('euiCollapsedItemActionsButton');
232+
const actionButton = within(table.rows[0]).getByTestId('euiCollapsedItemActionsButton');
235233
await user.click(actionButton);
236234

237235
expect(document.querySelector('.euiContextMenuPanel')).not.toBeNull();
238236
});
239237

240238
test('should have the "pause", "edit" and "unfollow" options in the row context menu', async () => {
241-
const rows = getTableRows('followerIndexListTable');
242-
const actionButton = within(rows[0]).getByTestId('euiCollapsedItemActionsButton');
239+
const actionButton = within(table.rows[0]).getByTestId('euiCollapsedItemActionsButton');
243240
await user.click(actionButton);
244241

245242
const contextMenuPanel = document.querySelector('.euiContextMenuPanel');
@@ -254,8 +251,7 @@ describe('<FollowerIndicesList />', () => {
254251
});
255252

256253
test('should have the "resume", "edit" and "unfollow" options in the row context menu for paused index', async () => {
257-
const rows = getTableRows('followerIndexListTable');
258-
const actionButton = within(rows[1]).getByTestId('euiCollapsedItemActionsButton');
254+
const actionButton = within(table.rows[1]).getByTestId('euiCollapsedItemActionsButton');
259255
await user.click(actionButton);
260256

261257
const contextMenuPanel = document.querySelector('.euiContextMenuPanel');
@@ -272,8 +268,7 @@ describe('<FollowerIndicesList />', () => {
272268
test('should open a confirmation modal when clicking on "pause replication"', async () => {
273269
expect(screen.queryByTestId('pauseReplicationConfirmation')).not.toBeInTheDocument();
274270

275-
const rows = getTableRows('followerIndexListTable');
276-
const actionButton = within(rows[0]).getByTestId('euiCollapsedItemActionsButton');
271+
const actionButton = within(table.rows[0]).getByTestId('euiCollapsedItemActionsButton');
277272
await user.click(actionButton);
278273

279274
const pauseButton = await screen.findByTestId('pauseButton');
@@ -285,8 +280,7 @@ describe('<FollowerIndicesList />', () => {
285280
test('should open a confirmation modal when clicking on "resume"', async () => {
286281
expect(screen.queryByTestId('resumeReplicationConfirmation')).not.toBeInTheDocument();
287282

288-
const rows = getTableRows('followerIndexListTable');
289-
const actionButton = within(rows[1]).getByTestId('euiCollapsedItemActionsButton');
283+
const actionButton = within(table.rows[1]).getByTestId('euiCollapsedItemActionsButton');
290284
await user.click(actionButton);
291285

292286
const resumeButton = await screen.findByTestId('resumeButton');
@@ -298,8 +292,7 @@ describe('<FollowerIndicesList />', () => {
298292
test('should open a confirmation modal when clicking on "unfollow leader index"', async () => {
299293
expect(screen.queryByTestId('unfollowLeaderConfirmation')).not.toBeInTheDocument();
300294

301-
const rows = getTableRows('followerIndexListTable');
302-
const actionButton = within(rows[0]).getByTestId('euiCollapsedItemActionsButton');
295+
const actionButton = within(table.rows[0]).getByTestId('euiCollapsedItemActionsButton');
303296
await user.click(actionButton);
304297

305298
const unfollowButton = await screen.findByTestId('unfollowButton');
@@ -313,8 +306,7 @@ describe('<FollowerIndicesList />', () => {
313306
test('should open a detail panel when clicking on a follower index', async () => {
314307
expect(screen.queryByTestId('followerIndexDetail')).not.toBeInTheDocument();
315308

316-
const rows = getTableRows('followerIndexListTable');
317-
const nameLink = within(rows[0]).getByText(index1.name);
309+
const nameLink = within(table.rows[0]).getByText(index1.name);
318310
await user.click(nameLink);
319311

320312
expect(await screen.findByTestId('followerIndexDetail')).toBeInTheDocument();

x-pack/platform/plugins/private/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { screen, within, act } from '@testing-library/react';
99
import { renderWithRouter } from './render';
10-
import { getTableRows } from './eui_table';
10+
import { EuiTableTestHarness } from '@kbn/test-eui-helpers';
1111
import { AutoFollowPatternList } from '../../../app/sections/home/auto_follow_pattern_list';
1212
import { createCrossClusterReplicationStore } from '../../../app/store';
1313
import { routing } from '../../../app/services/routing';
@@ -36,8 +36,8 @@ export const setup = (props = {}) => {
3636
// Helper actions for this specific page
3737
actions: {
3838
async selectAutoFollowPatternAt(index) {
39-
const rows = getTableRows('autoFollowPatternListTable');
40-
const checkbox = within(rows[index]).getByRole('checkbox');
39+
const table = new EuiTableTestHarness('autoFollowPatternListTable');
40+
const checkbox = within(table.rows[index]).getByRole('checkbox');
4141
await result.user.click(checkbox);
4242
},
4343

0 commit comments

Comments
 (0)