@rickcedwhat/playwright-smart-table 6.15.0 → 6.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.muiDataGrid = exports.muiTable = void 0;
4
+ exports.createMuiTable = createMuiTable;
5
+ exports.createMuiDataGrid = createMuiDataGrid;
4
6
  const debugUtils_1 = require("../utils/debugUtils");
7
+ const DEFAULT_LABELS = {
8
+ nextPage: 'Go to next page',
9
+ previousPage: 'Go to previous page',
10
+ firstPage: 'Go to first page',
11
+ };
5
12
  /**
6
13
  * Safely wait for MUI pagination to stabilize.
7
14
  * Polls for displayed row text changes and waits for loading overlays to disappear.
@@ -45,120 +52,136 @@ async function getPaginationRoot(context) {
45
52
  return context.root.locator('xpath=following::div[contains(@class, "MuiTablePagination-root")][1]').first();
46
53
  }
47
54
  /**
48
- * Preset configuration for the standard Material UI `<Table />` component.
55
+ * Creates a preset configuration for the standard Material UI `<Table />` component.
49
56
  *
50
- * Handles:
51
- * - Selectors for `.MuiTableRow-root`, `.MuiTableCell-head`, etc.
52
- * - Pagination via the standard `.MuiTablePagination-actions` footer.
53
- * - Sorting via the `.MuiTableSortLabel-root` header classes.
54
- * - Virtualized DOM deduplication (falls back to cell text if unvirtualized).
57
+ * @param opts.buttonLabels - Override aria-labels for pagination buttons (use for non-English locales).
58
+ *
59
+ * @example
60
+ * // French locale
61
+ * const table = useTable(loc, createMuiTable({ buttonLabels: { nextPage: 'Page suivante', previousPage: 'Page précédente' } }));
55
62
  */
56
- exports.muiTable = {
57
- rowSelector: 'tbody tr, tbody .MuiTableRow-root',
58
- cellSelector: 'td, th, .MuiTableCell-body',
59
- headerSelector: 'thead th, .MuiTableCell-head',
60
- headerTransformer: ({ text }) => text.replace(/\bsorted\s+(?:ascending|descending)\b/i, '').trim(),
61
- strategies: {
62
- pagination: {
63
- goNext: async (context) => {
64
- const root = await getPaginationRoot(context);
65
- const nextBtn = root.locator('button[aria-label="Go to next page"]');
66
- if (await nextBtn.count() === 0 || await nextBtn.isDisabled())
67
- return false;
68
- const displayedRows = root.locator('.MuiTablePagination-displayedRows');
69
- const oldText = await displayedRows.innerText().catch(() => '');
70
- // Force click ignores cookie banners obscuring the footer
71
- await nextBtn.click({ force: true });
72
- await waitForMuiPaginationStabilization(context, displayedRows, oldText);
73
- return true;
74
- },
75
- goPrevious: async (context) => {
76
- const root = await getPaginationRoot(context);
77
- const prevBtn = root.locator('button[aria-label="Go to previous page"]');
78
- if (await prevBtn.count() === 0 || await prevBtn.isDisabled())
79
- return false;
80
- const displayedRows = root.locator('.MuiTablePagination-displayedRows');
81
- const oldText = await displayedRows.innerText().catch(() => '');
82
- await prevBtn.click({ force: true });
83
- await waitForMuiPaginationStabilization(context, displayedRows, oldText);
84
- return true;
85
- },
86
- goToFirst: async (context) => {
87
- const root = await getPaginationRoot(context);
88
- const prevBtn = root.locator('button[aria-label="Go to previous page"]');
89
- const displayedRows = root.locator('.MuiTablePagination-displayedRows');
90
- let clicked = false;
91
- let noProgressCount = 0;
92
- const NO_PROGRESS_LIMIT = 3;
93
- while (await prevBtn.count() > 0 && !(await prevBtn.isDisabled())) {
63
+ function createMuiTable(opts) {
64
+ const labels = Object.assign(Object.assign({}, DEFAULT_LABELS), opts === null || opts === void 0 ? void 0 : opts.buttonLabels);
65
+ return {
66
+ rowSelector: 'tbody tr, tbody .MuiTableRow-root',
67
+ cellSelector: 'td, th, .MuiTableCell-body',
68
+ headerSelector: 'thead th, .MuiTableCell-head',
69
+ headerTransformer: ({ text }) => text.replace(/\bsorted\s+(?:ascending|descending)\b/i, '').trim(),
70
+ strategies: {
71
+ pagination: {
72
+ goNext: async (context) => {
73
+ const root = await getPaginationRoot(context);
74
+ const nextBtn = root.getByRole('button', { name: labels.nextPage, exact: true });
75
+ if (await nextBtn.count() === 0 || await nextBtn.isDisabled())
76
+ return false;
77
+ const displayedRows = root.locator('.MuiTablePagination-displayedRows');
94
78
  const oldText = await displayedRows.innerText().catch(() => '');
95
- await prevBtn.click();
79
+ // Force click ignores cookie banners obscuring the footer
80
+ await nextBtn.click({ force: true });
96
81
  await waitForMuiPaginationStabilization(context, displayedRows, oldText);
97
- clicked = true;
98
- const newText = await displayedRows.innerText().catch(() => '');
99
- if (newText === oldText) {
100
- noProgressCount++;
101
- if (noProgressCount >= NO_PROGRESS_LIMIT) {
102
- (0, debugUtils_1.logDebug)(context.config, 'error', `goToFirst: no pagination progress after ${NO_PROGRESS_LIMIT} consecutive clicks — aborting to avoid infinite loop`);
103
- return false;
104
- }
82
+ return true;
83
+ },
84
+ goPrevious: async (context) => {
85
+ const root = await getPaginationRoot(context);
86
+ const prevBtn = root.getByRole('button', { name: labels.previousPage, exact: true });
87
+ if (await prevBtn.count() === 0 || await prevBtn.isDisabled())
88
+ return false;
89
+ const displayedRows = root.locator('.MuiTablePagination-displayedRows');
90
+ const oldText = await displayedRows.innerText().catch(() => '');
91
+ await prevBtn.click({ force: true });
92
+ await waitForMuiPaginationStabilization(context, displayedRows, oldText);
93
+ return true;
94
+ },
95
+ goToFirst: async (context) => {
96
+ const root = await getPaginationRoot(context);
97
+ const displayedRows = root.locator('.MuiTablePagination-displayedRows');
98
+ // MUI TablePagination optionally renders a "first page" button (showFirstButton prop)
99
+ const firstBtn = root.getByRole('button', { name: labels.firstPage, exact: true });
100
+ if (await firstBtn.count() > 0 && !(await firstBtn.isDisabled())) {
101
+ const oldText = await displayedRows.innerText().catch(() => '');
102
+ await firstBtn.click({ force: true });
103
+ await waitForMuiPaginationStabilization(context, displayedRows, oldText);
104
+ return true;
105
105
  }
106
- else {
107
- noProgressCount = 0;
106
+ // Fall back to stepping backward one page at a time
107
+ const prevBtn = root.getByRole('button', { name: labels.previousPage, exact: true });
108
+ let clicked = false;
109
+ let noProgressCount = 0;
110
+ const NO_PROGRESS_LIMIT = 3;
111
+ while (await prevBtn.count() > 0 && !(await prevBtn.isDisabled())) {
112
+ const oldText = await displayedRows.innerText().catch(() => '');
113
+ await prevBtn.click({ force: true });
114
+ await waitForMuiPaginationStabilization(context, displayedRows, oldText);
115
+ clicked = true;
116
+ const newText = await displayedRows.innerText().catch(() => '');
117
+ if (newText === oldText) {
118
+ noProgressCount++;
119
+ if (noProgressCount >= NO_PROGRESS_LIMIT) {
120
+ (0, debugUtils_1.logDebug)(context.config, 'error', `goToFirst: no pagination progress after ${NO_PROGRESS_LIMIT} consecutive clicks — aborting to avoid infinite loop`);
121
+ return false;
122
+ }
123
+ }
124
+ else {
125
+ noProgressCount = 0;
126
+ }
108
127
  }
128
+ return clicked;
109
129
  }
110
- return clicked;
111
- }
112
- },
113
- sorting: {
114
- getSortState: async ({ columnName, context }) => {
115
- if (!context.getHeaderCell)
116
- return 'none';
117
- const header = await context.getHeaderCell(columnName);
118
- const sortLabel = header.locator('.MuiTableSortLabel-root').first();
119
- if (!(await sortLabel.isVisible().catch(() => false)))
120
- return 'none';
121
- const className = await sortLabel.getAttribute('class') || '';
122
- if (className.includes('directionAsc'))
123
- return 'asc';
124
- if (className.includes('directionDesc'))
125
- return 'desc';
126
- // Often, if the label is active but lacks explicit direction classes, MUI defaults to ascending
127
- if (className.includes('active'))
128
- return 'asc';
129
- return 'none';
130
130
  },
131
- doSort: async ({ columnName, direction, context }) => {
132
- var _a, _b;
133
- if (!context.getHeaderCell)
134
- return;
135
- const header = await context.getHeaderCell(columnName);
136
- const sortLabel = header.locator('.MuiTableSortLabel-root').first();
137
- const target = await sortLabel.isVisible().catch(() => false) ? sortLabel : header;
138
- let current = await ((_a = context.config.strategies.sorting) === null || _a === void 0 ? void 0 : _a.getSortState({ columnName, context })) || 'none';
139
- let attempts = 0;
140
- // Click until the state matches the target direction, max 3 times (none -> asc -> desc)
141
- while (current !== direction && attempts < 3) {
142
- await target.click();
143
- await context.page.waitForTimeout(100);
144
- current = await ((_b = context.config.strategies.sorting) === null || _b === void 0 ? void 0 : _b.getSortState({ columnName, context })) || 'none';
145
- attempts++;
131
+ sorting: {
132
+ getSortState: async ({ columnName, context }) => {
133
+ if (!context.getHeaderCell)
134
+ return 'none';
135
+ const header = await context.getHeaderCell(columnName);
136
+ const sortLabel = header.locator('.MuiTableSortLabel-root').first();
137
+ if (!(await sortLabel.isVisible().catch(() => false)))
138
+ return 'none';
139
+ const className = await sortLabel.getAttribute('class') || '';
140
+ if (className.includes('directionAsc'))
141
+ return 'asc';
142
+ if (className.includes('directionDesc'))
143
+ return 'desc';
144
+ // Often, if the label is active but lacks explicit direction classes, MUI defaults to ascending
145
+ if (className.includes('active'))
146
+ return 'asc';
147
+ return 'none';
148
+ },
149
+ doSort: async ({ columnName, direction, context }) => {
150
+ var _a, _b;
151
+ if (!context.getHeaderCell)
152
+ return;
153
+ const header = await context.getHeaderCell(columnName);
154
+ const sortLabel = header.locator('.MuiTableSortLabel-root').first();
155
+ const target = await sortLabel.isVisible().catch(() => false) ? sortLabel : header;
156
+ let current = await ((_a = context.config.strategies.sorting) === null || _a === void 0 ? void 0 : _a.getSortState({ columnName, context })) || 'none';
157
+ let attempts = 0;
158
+ // Click until the state matches the target direction, max 3 times (none -> asc -> desc)
159
+ while (current !== direction && attempts < 3) {
160
+ await target.click();
161
+ await context.page.waitForTimeout(100);
162
+ current = await ((_b = context.config.strategies.sorting) === null || _b === void 0 ? void 0 : _b.getSortState({ columnName, context })) || 'none';
163
+ attempts++;
164
+ }
146
165
  }
166
+ },
167
+ dedupe: async (row) => {
168
+ // MUI virtualized tables attach data-index to rows
169
+ const dataIndex = await row.getAttribute('data-index');
170
+ if (dataIndex !== null)
171
+ return dataIndex;
172
+ // Unvirtualized tables don't recycle nodes, but if they do duplicate, fallback to first cell's text
173
+ const text = await row.locator('td, th').first().innerText().catch(() => '');
174
+ return text || String(Math.random());
147
175
  }
148
- },
149
- dedupe: async (row) => {
150
- // MUI virtualized tables attach data-index to rows
151
- const dataIndex = await row.getAttribute('data-index');
152
- if (dataIndex !== null)
153
- return dataIndex;
154
- // Unvirtualized tables don't recycle nodes, but if they do duplicate, fallback to first cell's text
155
- const text = await row.locator('td, th').first().innerText().catch(() => '');
156
- return text || String(Math.random());
157
176
  }
158
- }
159
- };
177
+ };
178
+ }
179
+ /** Default preset for MUI `<Table />`. For non-English locales use `createMuiTable({ buttonLabels })`. */
180
+ exports.muiTable = createMuiTable();
160
181
  /**
161
- * Preset configuration for the MUI DataGrid component (@mui/x-data-grid).
182
+ * Creates a preset configuration for the MUI DataGrid component (@mui/x-data-grid).
183
+ *
184
+ * @param opts.buttonLabels - Override aria-labels for pagination buttons (use for non-English locales).
162
185
  *
163
186
  * Handles:
164
187
  * - Selectors for `.MuiDataGrid-row`, `.MuiDataGrid-cell`, and `.MuiDataGrid-columnHeader`.
@@ -167,186 +190,238 @@ exports.muiTable = {
167
190
  * - Vertical Virtualization deduplication using `data-rowindex`.
168
191
  * - Horizontal Virtualization support via `aria-colindex`.
169
192
  */
170
- exports.muiDataGrid = {
171
- rowSelector: '.MuiDataGrid-row',
172
- cellSelector: '.MuiDataGrid-cell',
173
- headerSelector: '.MuiDataGrid-columnHeader',
174
- strategies: {
175
- pagination: {
176
- goNext: async (context) => {
177
- const footer = context.root.locator('.MuiDataGrid-footerContainer');
178
- const nextBtn = footer.locator('button[aria-label="Go to next page"]');
179
- try {
180
- await nextBtn.waitFor({ state: 'visible', timeout: 2000 });
181
- }
182
- catch (e) {
183
- return false;
193
+ function createMuiDataGrid(opts) {
194
+ const labels = Object.assign(Object.assign({}, DEFAULT_LABELS), opts === null || opts === void 0 ? void 0 : opts.buttonLabels);
195
+ return {
196
+ rowSelector: '.MuiDataGrid-row',
197
+ cellSelector: '.MuiDataGrid-cell',
198
+ headerSelector: '.MuiDataGrid-columnHeader',
199
+ strategies: {
200
+ pagination: {
201
+ goNext: async (context) => {
202
+ const footer = context.root.locator('.MuiDataGrid-footerContainer');
203
+ const nextBtn = footer.getByRole('button', { name: labels.nextPage, exact: true });
204
+ try {
205
+ await nextBtn.waitFor({ state: 'visible', timeout: 2000 });
206
+ }
207
+ catch (e) {
208
+ return false;
209
+ }
210
+ if (await nextBtn.isDisabled())
211
+ return false;
212
+ const displayedRows = footer.locator('.MuiTablePagination-displayedRows');
213
+ const oldText = await displayedRows.innerText().catch(() => '');
214
+ await nextBtn.scrollIntoViewIfNeeded().catch(() => { });
215
+ await nextBtn.click({ force: true });
216
+ await waitForMuiPaginationStabilization(context, displayedRows, oldText);
217
+ return true;
218
+ },
219
+ goPrevious: async (context) => {
220
+ const footer = context.root.locator('.MuiDataGrid-footerContainer');
221
+ const prevBtn = footer.getByRole('button', { name: labels.previousPage, exact: true });
222
+ try {
223
+ await prevBtn.waitFor({ state: 'visible', timeout: 2000 });
224
+ }
225
+ catch (e) {
226
+ return false;
227
+ }
228
+ if (await prevBtn.isDisabled())
229
+ return false;
230
+ const displayedRows = footer.locator('.MuiTablePagination-displayedRows');
231
+ const oldText = await displayedRows.innerText().catch(() => '');
232
+ await prevBtn.scrollIntoViewIfNeeded().catch(() => { });
233
+ await prevBtn.click({ force: true });
234
+ await waitForMuiPaginationStabilization(context, displayedRows, oldText);
235
+ return true;
236
+ },
237
+ goToFirst: async (context) => {
238
+ const footer = context.root.locator('.MuiDataGrid-footerContainer');
239
+ const displayedRows = footer.locator('.MuiTablePagination-displayedRows');
240
+ const firstBtn = footer.getByRole('button', { name: labels.firstPage, exact: true });
241
+ try {
242
+ await firstBtn.waitFor({ state: 'visible', timeout: 1000 });
243
+ if (!(await firstBtn.isDisabled())) {
244
+ const oldText = await displayedRows.innerText().catch(() => '');
245
+ await firstBtn.scrollIntoViewIfNeeded().catch(() => { });
246
+ await firstBtn.click({ force: true });
247
+ await waitForMuiPaginationStabilization(context, displayedRows, oldText);
248
+ return true;
249
+ }
250
+ }
251
+ catch (_a) {
252
+ // first-page button not present; fall back to stepping backward
253
+ }
254
+ const prevBtn = footer.getByRole('button', { name: labels.previousPage, exact: true });
255
+ let clicked = false;
256
+ let noProgressCount = 0;
257
+ const NO_PROGRESS_LIMIT = 3;
258
+ while (true) {
259
+ try {
260
+ await prevBtn.waitFor({ state: 'visible', timeout: 2000 });
261
+ }
262
+ catch (_b) {
263
+ break;
264
+ }
265
+ if (await prevBtn.isDisabled())
266
+ break;
267
+ const oldText = await displayedRows.innerText().catch(() => '');
268
+ await prevBtn.scrollIntoViewIfNeeded().catch(() => { });
269
+ await prevBtn.click({ force: true });
270
+ await waitForMuiPaginationStabilization(context, displayedRows, oldText);
271
+ clicked = true;
272
+ const newText = await displayedRows.innerText().catch(() => '');
273
+ if (newText === oldText) {
274
+ noProgressCount++;
275
+ if (noProgressCount >= NO_PROGRESS_LIMIT) {
276
+ (0, debugUtils_1.logDebug)(context.config, 'error', `goToFirst: no pagination progress after ${NO_PROGRESS_LIMIT} consecutive clicks — aborting to avoid infinite loop`);
277
+ return false;
278
+ }
279
+ }
280
+ else {
281
+ noProgressCount = 0;
282
+ }
283
+ }
284
+ return clicked;
184
285
  }
185
- if (await nextBtn.isDisabled())
186
- return false;
187
- const displayedRows = footer.locator('.MuiTablePagination-displayedRows');
188
- const oldText = await displayedRows.innerText().catch(() => '');
189
- await nextBtn.scrollIntoViewIfNeeded().catch(() => { });
190
- await nextBtn.click({ force: true });
191
- await waitForMuiPaginationStabilization(context, displayedRows, oldText);
192
- return true;
193
286
  },
194
- goPrevious: async (context) => {
195
- const footer = context.root.locator('.MuiDataGrid-footerContainer');
196
- const prevBtn = footer.locator('button[aria-label="Go to previous page"]');
197
- try {
198
- await prevBtn.waitFor({ state: 'visible', timeout: 2000 });
199
- }
200
- catch (e) {
201
- return false;
202
- }
203
- if (await prevBtn.isDisabled())
204
- return false;
205
- const displayedRows = footer.locator('.MuiTablePagination-displayedRows');
206
- const oldText = await displayedRows.innerText().catch(() => '');
207
- await prevBtn.scrollIntoViewIfNeeded().catch(() => { });
208
- await prevBtn.click({ force: true });
209
- await waitForMuiPaginationStabilization(context, displayedRows, oldText);
210
- return true;
211
- }
212
- },
213
- sorting: {
214
- getSortState: async ({ columnName, context }) => {
215
- if (!context.getHeaderCell)
287
+ sorting: {
288
+ getSortState: async ({ columnName, context }) => {
289
+ if (!context.getHeaderCell)
290
+ return 'none';
291
+ const header = await context.getHeaderCell(columnName);
292
+ const sortAttr = await header.getAttribute('aria-sort');
293
+ if (sortAttr === 'ascending')
294
+ return 'asc';
295
+ if (sortAttr === 'descending')
296
+ return 'desc';
216
297
  return 'none';
217
- const header = await context.getHeaderCell(columnName);
218
- const sortAttr = await header.getAttribute('aria-sort');
219
- if (sortAttr === 'ascending')
220
- return 'asc';
221
- if (sortAttr === 'descending')
222
- return 'desc';
223
- return 'none';
224
- },
225
- doSort: async ({ columnName, direction, context }) => {
226
- var _a, _b;
227
- if (!context.getHeaderCell)
228
- return;
229
- const header = await context.getHeaderCell(columnName);
230
- let current = await ((_a = context.config.strategies.sorting) === null || _a === void 0 ? void 0 : _a.getSortState({ columnName, context })) || 'none';
231
- let attempts = 0;
232
- while (current !== direction && attempts < 3) {
233
- const clickTarget = header.locator('.MuiDataGrid-columnHeaderTitleContainer').first();
234
- if (await clickTarget.isVisible()) {
235
- await clickTarget.click({ force: true });
236
- }
237
- else {
238
- await header.click({ force: true });
298
+ },
299
+ doSort: async ({ columnName, direction, context }) => {
300
+ var _a, _b;
301
+ if (!context.getHeaderCell)
302
+ return;
303
+ const header = await context.getHeaderCell(columnName);
304
+ let current = await ((_a = context.config.strategies.sorting) === null || _a === void 0 ? void 0 : _a.getSortState({ columnName, context })) || 'none';
305
+ let attempts = 0;
306
+ while (current !== direction && attempts < 3) {
307
+ const clickTarget = header.locator('.MuiDataGrid-columnHeaderTitleContainer').first();
308
+ if (await clickTarget.isVisible()) {
309
+ await clickTarget.click({ force: true });
310
+ }
311
+ else {
312
+ await header.click({ force: true });
313
+ }
314
+ // Wait for stabilization (using DataGrid specific overlay check inside helper)
315
+ const displayedRows = context.root.locator('.MuiTablePagination-displayedRows');
316
+ const text = await displayedRows.innerText().catch(() => '');
317
+ await waitForMuiPaginationStabilization(context, displayedRows, text);
318
+ current = await ((_b = context.config.strategies.sorting) === null || _b === void 0 ? void 0 : _b.getSortState({ columnName, context })) || 'none';
319
+ attempts++;
239
320
  }
240
- // Sorting can trigger large re-renders/virtualization shifts
241
- await context.page.waitForTimeout(500);
242
- // Wait for stabilization (using DataGrid specific overlay check inside helper)
243
- const displayedRows = context.root.locator('.MuiTablePagination-displayedRows');
244
- const text = await displayedRows.innerText().catch(() => '');
245
- await waitForMuiPaginationStabilization(context, displayedRows, text);
246
- current = await ((_b = context.config.strategies.sorting) === null || _b === void 0 ? void 0 : _b.getSortState({ columnName, context })) || 'none';
247
- attempts++;
248
321
  }
249
- }
250
- },
251
- dedupe: async (row) => {
252
- var _a;
253
- // DataGrid uses data-rowindex for its own virtualization management
254
- return (_a = (await row.getAttribute('data-rowindex').catch(() => null))) !== null && _a !== void 0 ? _a : '';
255
- },
256
- getCellLocator: ({ row, columnIndex }) => {
257
- // Horizontal virtualization uses aria-colindex (1-indexed)
258
- return row.locator(`[aria-colindex="${columnIndex + 1}"]`);
259
- },
260
- // MUI DataGrid virtualizes rows vertically (always) and columns horizontally (when
261
- // columnBuffer is configured). The virtualScroller is a descendant of root, so
262
- // scroll methods use querySelector rather than closest().
263
- viewport: {
264
- getVisibleRowRange: async ({ root, config }) => {
265
- const rowSel = config.rowSelector;
266
- return root.evaluate((el, rowSel) => {
267
- const indices = Array.from(el.querySelectorAll(rowSel))
268
- .map(r => Number(r.getAttribute('data-rowindex')))
269
- .filter(n => !isNaN(n));
270
- if (!indices.length)
271
- return { first: 0, last: 0 };
272
- return { first: Math.min(...indices), last: Math.max(...indices) };
273
- }, rowSel);
274
322
  },
275
- getVisibleColumnRange: async ({ root, config }) => {
276
- const rowSel = config.rowSelector;
277
- const cellSel = typeof config.cellSelector === 'string' ? config.cellSelector : '[aria-colindex]';
278
- return root.evaluate((el, { rowSel, cellSel }) => {
279
- const firstRow = el.querySelector(rowSel);
280
- if (!firstRow)
281
- return { first: 0, last: 0 };
282
- const indices = Array.from(firstRow.querySelectorAll(cellSel))
283
- .map(c => Number(c.getAttribute('aria-colindex')) - 1)
284
- .filter(n => !isNaN(n));
285
- if (!indices.length)
286
- return { first: 0, last: 0 };
287
- return { first: Math.min(...indices), last: Math.max(...indices) };
288
- }, { rowSel, cellSel });
323
+ dedupe: async (row) => {
324
+ var _a;
325
+ // DataGrid uses data-rowindex for its own virtualization management
326
+ return (_a = (await row.getAttribute('data-rowindex').catch(() => null))) !== null && _a !== void 0 ? _a : '';
289
327
  },
290
- scrollToRow: async ({ root, config }, rowIndex) => {
291
- const rowSel = config.rowSelector;
292
- await root.evaluate((el, { rowSel, idx }) => {
293
- const scroller = el.querySelector('.MuiDataGrid-virtualScroller');
294
- if (!scroller)
295
- return;
296
- const row = el.querySelector(`${rowSel}[data-rowindex="${idx}"]`);
297
- if (row) {
298
- row.scrollIntoView({ block: 'nearest', inline: 'nearest' });
299
- return;
300
- }
301
- const visibleRows = Array.from(el.querySelectorAll(rowSel));
302
- const heights = visibleRows
303
- .map(visibleRow => visibleRow.getBoundingClientRect().height)
304
- .filter(height => height > 0);
305
- const estimatedHeight = heights.length
306
- ? heights.reduce((sum, height) => sum + height, 0) / heights.length
307
- : 52;
308
- scroller.scrollTop = Math.max(0, idx * estimatedHeight - 20);
309
- }, { rowSel, idx: rowIndex });
310
- await root.locator(`${rowSel}[data-rowindex="${rowIndex}"]`)
311
- .waitFor({ state: 'attached', timeout: 3000 });
328
+ getCellLocator: ({ row, columnIndex }) => {
329
+ // Horizontal virtualization uses aria-colindex (1-indexed)
330
+ return row.locator(`[aria-colindex="${columnIndex + 1}"]`);
312
331
  },
313
- scrollToColumn: async ({ root, config }, colIndex) => {
314
- const headerSel = typeof config.headerSelector === 'string' ? config.headerSelector : null;
315
- await root.evaluate((el, { headerSel, idx }) => {
316
- // virtualScroller is a descendant — querySelector, not closest
317
- const scroller = el.querySelector('.MuiDataGrid-virtualScroller');
318
- if (!scroller || !headerSel)
319
- return;
320
- const headers = Array.from(el.querySelectorAll(headerSel));
321
- const target = headers[idx];
322
- if (!target) {
323
- const widths = headers
324
- .map(header => header.getBoundingClientRect().width)
325
- .filter(width => width > 0);
326
- const estimatedWidth = widths.length
327
- ? widths.reduce((sum, width) => sum + width, 0) / widths.length
328
- : 100;
329
- scroller.scrollLeft = Math.max(0, idx * estimatedWidth - 20);
330
- return;
331
- }
332
- const cRect = scroller.getBoundingClientRect();
333
- const tRect = target.getBoundingClientRect();
334
- if (tRect.left < cRect.left) {
335
- scroller.scrollLeft -= (cRect.left - tRect.left) + 20;
336
- }
337
- else if (tRect.right > cRect.right) {
338
- scroller.scrollLeft += (tRect.right - cRect.right) + 20;
339
- }
340
- }, { headerSel, idx: colIndex });
341
- await root.locator(`${config.rowSelector} [aria-colindex="${colIndex + 1}"]`)
342
- .first()
343
- .waitFor({ state: 'attached', timeout: 3000 });
332
+ // MUI DataGrid virtualizes rows vertically (always) and columns horizontally (when
333
+ // columnBuffer is configured). The virtualScroller is a descendant of root, so
334
+ // scroll methods use querySelector rather than closest().
335
+ viewport: {
336
+ getVisibleRowRange: async ({ root, config }) => {
337
+ const rowSel = config.rowSelector;
338
+ return root.evaluate((el, rowSel) => {
339
+ const indices = Array.from(el.querySelectorAll(rowSel))
340
+ .map(r => Number(r.getAttribute('data-rowindex')))
341
+ .filter(n => !isNaN(n));
342
+ if (!indices.length)
343
+ return { first: 0, last: 0 };
344
+ return { first: Math.min(...indices), last: Math.max(...indices) };
345
+ }, rowSel);
346
+ },
347
+ getVisibleColumnRange: async ({ root, config }) => {
348
+ const rowSel = config.rowSelector;
349
+ const cellSel = typeof config.cellSelector === 'string' ? config.cellSelector : '[aria-colindex]';
350
+ return root.evaluate((el, { rowSel, cellSel }) => {
351
+ const firstRow = el.querySelector(rowSel);
352
+ if (!firstRow)
353
+ return { first: 0, last: 0 };
354
+ const indices = Array.from(firstRow.querySelectorAll(cellSel))
355
+ .map(c => Number(c.getAttribute('aria-colindex')) - 1)
356
+ .filter(n => !isNaN(n));
357
+ if (!indices.length)
358
+ return { first: 0, last: 0 };
359
+ return { first: Math.min(...indices), last: Math.max(...indices) };
360
+ }, { rowSel, cellSel });
361
+ },
362
+ scrollToRow: async ({ root, config }, rowIndex) => {
363
+ const rowSel = config.rowSelector;
364
+ await root.evaluate((el, { rowSel, idx }) => {
365
+ const scroller = el.querySelector('.MuiDataGrid-virtualScroller');
366
+ if (!scroller)
367
+ return;
368
+ const row = el.querySelector(`${rowSel}[data-rowindex="${idx}"]`);
369
+ if (row) {
370
+ row.scrollIntoView({ block: 'nearest', inline: 'nearest' });
371
+ return;
372
+ }
373
+ const visibleRows = Array.from(el.querySelectorAll(rowSel));
374
+ const heights = visibleRows
375
+ .map(visibleRow => visibleRow.getBoundingClientRect().height)
376
+ .filter(height => height > 0);
377
+ const estimatedHeight = heights.length
378
+ ? heights.reduce((sum, height) => sum + height, 0) / heights.length
379
+ : 52;
380
+ scroller.scrollTop = Math.max(0, idx * estimatedHeight - 20);
381
+ }, { rowSel, idx: rowIndex });
382
+ await root.locator(`${rowSel}[data-rowindex="${rowIndex}"]`)
383
+ .waitFor({ state: 'attached', timeout: 3000 });
384
+ },
385
+ scrollToColumn: async ({ root, config }, colIndex) => {
386
+ const headerSel = typeof config.headerSelector === 'string' ? config.headerSelector : null;
387
+ await root.evaluate((el, { headerSel, idx }) => {
388
+ // virtualScroller is a descendant — querySelector, not closest
389
+ const scroller = el.querySelector('.MuiDataGrid-virtualScroller');
390
+ if (!scroller || !headerSel)
391
+ return;
392
+ const headers = Array.from(el.querySelectorAll(headerSel));
393
+ const target = headers[idx];
394
+ if (!target) {
395
+ const widths = headers
396
+ .map(header => header.getBoundingClientRect().width)
397
+ .filter(width => width > 0);
398
+ const estimatedWidth = widths.length
399
+ ? widths.reduce((sum, width) => sum + width, 0) / widths.length
400
+ : 100;
401
+ scroller.scrollLeft = Math.max(0, idx * estimatedWidth - 20);
402
+ return;
403
+ }
404
+ const cRect = scroller.getBoundingClientRect();
405
+ const tRect = target.getBoundingClientRect();
406
+ if (tRect.left < cRect.left) {
407
+ scroller.scrollLeft -= (cRect.left - tRect.left) + 20;
408
+ }
409
+ else if (tRect.right > cRect.right) {
410
+ scroller.scrollLeft += (tRect.right - cRect.right) + 20;
411
+ }
412
+ }, { headerSel, idx: colIndex });
413
+ await root.locator(`${config.rowSelector} [aria-colindex="${colIndex + 1}"]`)
414
+ .first()
415
+ .waitFor({ state: 'attached', timeout: 3000 });
416
+ },
344
417
  },
345
- },
346
- loading: {
347
- isTableLoading: async (context) => {
348
- return await context.root.locator('.MuiDataGrid-overlay').isVisible().catch(() => false);
418
+ loading: {
419
+ isTableLoading: async (context) => {
420
+ return await context.root.locator('.MuiDataGrid-overlay').isVisible().catch(() => false);
421
+ }
349
422
  }
350
423
  }
351
- }
352
- };
424
+ };
425
+ }
426
+ /** Default preset for MUI DataGrid. For non-English locales use `createMuiDataGrid({ buttonLabels })`. */
427
+ exports.muiDataGrid = createMuiDataGrid();