@rickcedwhat/playwright-smart-table 6.16.0 → 6.17.1
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.
- package/dist/engine/rowFinder.js +15 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/presets/index.d.ts +2 -1
- package/dist/presets/index.js +3 -1
- package/dist/presets/mui.d.ts +23 -7
- package/dist/presets/mui.js +441 -271
- package/dist/presets/rdg.d.ts +20 -13
- package/dist/presets/rdg.js +5 -5
- package/dist/smartRow.js +34 -27
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +44 -0
- package/dist/types.d.ts +39 -0
- package/dist/useTable.js +19 -0
- package/dist/utils/mergeTableConfig.d.ts +22 -0
- package/dist/utils/mergeTableConfig.js +60 -0
- package/dist/utils/navigationBarrier.d.ts +19 -7
- package/dist/utils/navigationBarrier.js +57 -28
- package/package.json +1 -1
package/dist/presets/mui.js
CHANGED
|
@@ -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
|
-
*
|
|
55
|
+
* Creates a preset configuration for the standard Material UI `<Table />` component.
|
|
49
56
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
79
|
+
// Force click ignores cookie banners obscuring the footer
|
|
80
|
+
await nextBtn.click({ force: true });
|
|
96
81
|
await waitForMuiPaginationStabilization(context, displayedRows, oldText);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
*
|
|
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,184 +190,331 @@ exports.muiTable = {
|
|
|
167
190
|
* - Vertical Virtualization deduplication using `data-rowindex`.
|
|
168
191
|
* - Horizontal Virtualization support via `aria-colindex`.
|
|
169
192
|
*/
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
catch (e) {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
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
|
-
},
|
|
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)
|
|
216
|
-
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';
|
|
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
|
+
concurrency: 'synchronized',
|
|
200
|
+
strategies: {
|
|
201
|
+
resolveRowIndex: async (row) => {
|
|
202
|
+
const v = await row.getAttribute('data-rowindex').catch(() => null);
|
|
203
|
+
return v !== null && !isNaN(Number(v)) ? Number(v) : undefined;
|
|
224
204
|
},
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (await clickTarget.isVisible()) {
|
|
235
|
-
await clickTarget.click({ force: true });
|
|
205
|
+
pagination: {
|
|
206
|
+
goNext: async (context) => {
|
|
207
|
+
const footer = context.root.locator('.MuiDataGrid-footerContainer');
|
|
208
|
+
const nextBtn = footer.getByRole('button', { name: labels.nextPage, exact: true });
|
|
209
|
+
try {
|
|
210
|
+
await nextBtn.waitFor({ state: 'visible', timeout: 2000 });
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
return false;
|
|
236
214
|
}
|
|
237
|
-
|
|
238
|
-
|
|
215
|
+
if (await nextBtn.isDisabled())
|
|
216
|
+
return false;
|
|
217
|
+
const displayedRows = footer.locator('.MuiTablePagination-displayedRows');
|
|
218
|
+
const oldText = await displayedRows.innerText().catch(() => '');
|
|
219
|
+
await nextBtn.scrollIntoViewIfNeeded().catch(() => { });
|
|
220
|
+
await nextBtn.click({ force: true });
|
|
221
|
+
await waitForMuiPaginationStabilization(context, displayedRows, oldText);
|
|
222
|
+
return true;
|
|
223
|
+
},
|
|
224
|
+
goPrevious: async (context) => {
|
|
225
|
+
const footer = context.root.locator('.MuiDataGrid-footerContainer');
|
|
226
|
+
const prevBtn = footer.getByRole('button', { name: labels.previousPage, exact: true });
|
|
227
|
+
try {
|
|
228
|
+
await prevBtn.waitFor({ state: 'visible', timeout: 2000 });
|
|
239
229
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
await
|
|
244
|
-
|
|
245
|
-
|
|
230
|
+
catch (e) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
if (await prevBtn.isDisabled())
|
|
234
|
+
return false;
|
|
235
|
+
const displayedRows = footer.locator('.MuiTablePagination-displayedRows');
|
|
236
|
+
const oldText = await displayedRows.innerText().catch(() => '');
|
|
237
|
+
await prevBtn.scrollIntoViewIfNeeded().catch(() => { });
|
|
238
|
+
await prevBtn.click({ force: true });
|
|
239
|
+
await waitForMuiPaginationStabilization(context, displayedRows, oldText);
|
|
240
|
+
return true;
|
|
241
|
+
},
|
|
242
|
+
goToFirst: async (context) => {
|
|
243
|
+
const footer = context.root.locator('.MuiDataGrid-footerContainer');
|
|
244
|
+
const displayedRows = footer.locator('.MuiTablePagination-displayedRows');
|
|
245
|
+
const firstBtn = footer.getByRole('button', { name: labels.firstPage, exact: true });
|
|
246
|
+
try {
|
|
247
|
+
await firstBtn.waitFor({ state: 'visible', timeout: 1000 });
|
|
248
|
+
if (!(await firstBtn.isDisabled())) {
|
|
249
|
+
const oldText = await displayedRows.innerText().catch(() => '');
|
|
250
|
+
await firstBtn.scrollIntoViewIfNeeded().catch(() => { });
|
|
251
|
+
await firstBtn.click({ force: true });
|
|
252
|
+
await waitForMuiPaginationStabilization(context, displayedRows, oldText);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (_a) {
|
|
257
|
+
// first-page button not present; fall back to stepping backward
|
|
258
|
+
}
|
|
259
|
+
const prevBtn = footer.getByRole('button', { name: labels.previousPage, exact: true });
|
|
260
|
+
let clicked = false;
|
|
261
|
+
let noProgressCount = 0;
|
|
262
|
+
const NO_PROGRESS_LIMIT = 3;
|
|
263
|
+
while (true) {
|
|
264
|
+
try {
|
|
265
|
+
await prevBtn.waitFor({ state: 'visible', timeout: 2000 });
|
|
266
|
+
}
|
|
267
|
+
catch (_b) {
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
if (await prevBtn.isDisabled())
|
|
271
|
+
break;
|
|
272
|
+
const oldText = await displayedRows.innerText().catch(() => '');
|
|
273
|
+
await prevBtn.scrollIntoViewIfNeeded().catch(() => { });
|
|
274
|
+
await prevBtn.click({ force: true });
|
|
275
|
+
await waitForMuiPaginationStabilization(context, displayedRows, oldText);
|
|
276
|
+
clicked = true;
|
|
277
|
+
const newText = await displayedRows.innerText().catch(() => '');
|
|
278
|
+
if (newText === oldText) {
|
|
279
|
+
noProgressCount++;
|
|
280
|
+
if (noProgressCount >= NO_PROGRESS_LIMIT) {
|
|
281
|
+
(0, debugUtils_1.logDebug)(context.config, 'error', `goToFirst: no pagination progress after ${NO_PROGRESS_LIMIT} consecutive clicks — aborting to avoid infinite loop`);
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
noProgressCount = 0;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return clicked;
|
|
246
290
|
}
|
|
247
|
-
}
|
|
248
|
-
},
|
|
249
|
-
dedupe: async (row) => {
|
|
250
|
-
var _a;
|
|
251
|
-
// DataGrid uses data-rowindex for its own virtualization management
|
|
252
|
-
return (_a = (await row.getAttribute('data-rowindex').catch(() => null))) !== null && _a !== void 0 ? _a : '';
|
|
253
|
-
},
|
|
254
|
-
getCellLocator: ({ row, columnIndex }) => {
|
|
255
|
-
// Horizontal virtualization uses aria-colindex (1-indexed)
|
|
256
|
-
return row.locator(`[aria-colindex="${columnIndex + 1}"]`);
|
|
257
|
-
},
|
|
258
|
-
// MUI DataGrid virtualizes rows vertically (always) and columns horizontally (when
|
|
259
|
-
// columnBuffer is configured). The virtualScroller is a descendant of root, so
|
|
260
|
-
// scroll methods use querySelector rather than closest().
|
|
261
|
-
viewport: {
|
|
262
|
-
getVisibleRowRange: async ({ root, config }) => {
|
|
263
|
-
const rowSel = config.rowSelector;
|
|
264
|
-
return root.evaluate((el, rowSel) => {
|
|
265
|
-
const indices = Array.from(el.querySelectorAll(rowSel))
|
|
266
|
-
.map(r => Number(r.getAttribute('data-rowindex')))
|
|
267
|
-
.filter(n => !isNaN(n));
|
|
268
|
-
if (!indices.length)
|
|
269
|
-
return { first: 0, last: 0 };
|
|
270
|
-
return { first: Math.min(...indices), last: Math.max(...indices) };
|
|
271
|
-
}, rowSel);
|
|
272
291
|
},
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
scrollToRow: async ({ root, config }, rowIndex) => {
|
|
289
|
-
const rowSel = config.rowSelector;
|
|
290
|
-
await root.evaluate((el, { rowSel, idx }) => {
|
|
291
|
-
const scroller = el.querySelector('.MuiDataGrid-virtualScroller');
|
|
292
|
-
if (!scroller)
|
|
293
|
-
return;
|
|
294
|
-
const row = el.querySelector(`${rowSel}[data-rowindex="${idx}"]`);
|
|
295
|
-
if (row) {
|
|
296
|
-
row.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
292
|
+
sorting: {
|
|
293
|
+
getSortState: async ({ columnName, context }) => {
|
|
294
|
+
if (!context.getHeaderCell)
|
|
295
|
+
return 'none';
|
|
296
|
+
const header = await context.getHeaderCell(columnName);
|
|
297
|
+
const sortAttr = await header.getAttribute('aria-sort');
|
|
298
|
+
if (sortAttr === 'ascending')
|
|
299
|
+
return 'asc';
|
|
300
|
+
if (sortAttr === 'descending')
|
|
301
|
+
return 'desc';
|
|
302
|
+
return 'none';
|
|
303
|
+
},
|
|
304
|
+
doSort: async ({ columnName, direction, context }) => {
|
|
305
|
+
var _a, _b;
|
|
306
|
+
if (!context.getHeaderCell)
|
|
297
307
|
return;
|
|
308
|
+
const header = await context.getHeaderCell(columnName);
|
|
309
|
+
let current = await ((_a = context.config.strategies.sorting) === null || _a === void 0 ? void 0 : _a.getSortState({ columnName, context })) || 'none';
|
|
310
|
+
let attempts = 0;
|
|
311
|
+
while (current !== direction && attempts < 3) {
|
|
312
|
+
const clickTarget = header.locator('.MuiDataGrid-columnHeaderTitleContainer').first();
|
|
313
|
+
if (await clickTarget.isVisible()) {
|
|
314
|
+
await clickTarget.click({ force: true });
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
await header.click({ force: true });
|
|
318
|
+
}
|
|
319
|
+
// Wait for stabilization (using DataGrid specific overlay check inside helper)
|
|
320
|
+
const displayedRows = context.root.locator('.MuiTablePagination-displayedRows');
|
|
321
|
+
const text = await displayedRows.innerText().catch(() => '');
|
|
322
|
+
await waitForMuiPaginationStabilization(context, displayedRows, text);
|
|
323
|
+
current = await ((_b = context.config.strategies.sorting) === null || _b === void 0 ? void 0 : _b.getSortState({ columnName, context })) || 'none';
|
|
324
|
+
attempts++;
|
|
298
325
|
}
|
|
299
|
-
|
|
300
|
-
const heights = visibleRows
|
|
301
|
-
.map(visibleRow => visibleRow.getBoundingClientRect().height)
|
|
302
|
-
.filter(height => height > 0);
|
|
303
|
-
const estimatedHeight = heights.length
|
|
304
|
-
? heights.reduce((sum, height) => sum + height, 0) / heights.length
|
|
305
|
-
: 52;
|
|
306
|
-
scroller.scrollTop = Math.max(0, idx * estimatedHeight - 20);
|
|
307
|
-
}, { rowSel, idx: rowIndex });
|
|
308
|
-
await root.locator(`${rowSel}[data-rowindex="${rowIndex}"]`)
|
|
309
|
-
.waitFor({ state: 'attached', timeout: 3000 });
|
|
326
|
+
}
|
|
310
327
|
},
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
328
|
+
dedupe: async (row) => {
|
|
329
|
+
var _a;
|
|
330
|
+
// DataGrid uses data-rowindex for its own virtualization management
|
|
331
|
+
return (_a = (await row.getAttribute('data-rowindex').catch(() => null))) !== null && _a !== void 0 ? _a : '';
|
|
332
|
+
},
|
|
333
|
+
getCellLocator: ({ row, root, columnIndex, rowIndex }) => {
|
|
334
|
+
// Use data-rowindex for a stable row reference: nth-based locators break after
|
|
335
|
+
// vertical eviction because the DOM order shifts when rows are removed/added.
|
|
336
|
+
const stableRow = typeof rowIndex === 'number'
|
|
337
|
+
? root.locator(`[data-rowindex="${rowIndex}"]`)
|
|
338
|
+
: row;
|
|
339
|
+
return stableRow.locator(`[aria-colindex="${columnIndex + 1}"]`);
|
|
340
|
+
},
|
|
341
|
+
// MUI DataGrid virtualizes rows vertically (always) and columns horizontally (when
|
|
342
|
+
// columnBuffer is configured). The virtualScroller is a descendant of root, so
|
|
343
|
+
// scroll methods use querySelector rather than closest().
|
|
344
|
+
viewport: {
|
|
345
|
+
getVisibleRowRange: async ({ root, config }) => {
|
|
346
|
+
const rowSel = config.rowSelector;
|
|
347
|
+
return root.evaluate((el, rowSel) => {
|
|
348
|
+
const indices = Array.from(el.querySelectorAll(rowSel))
|
|
349
|
+
.map(r => Number(r.getAttribute('data-rowindex')))
|
|
350
|
+
.filter(n => !isNaN(n));
|
|
351
|
+
if (!indices.length)
|
|
352
|
+
return { first: 0, last: 0 };
|
|
353
|
+
return { first: Math.min(...indices), last: Math.max(...indices) };
|
|
354
|
+
}, rowSel);
|
|
355
|
+
},
|
|
356
|
+
getVisibleColumnRange: async ({ root, config }) => {
|
|
357
|
+
const rowSel = config.rowSelector;
|
|
358
|
+
const cellSel = typeof config.cellSelector === 'string' ? config.cellSelector : '[aria-colindex]';
|
|
359
|
+
return root.evaluate((el, { rowSel, cellSel }) => {
|
|
360
|
+
const scroller = el.querySelector('.MuiDataGrid-virtualScroller');
|
|
361
|
+
const allRows = Array.from(el.querySelectorAll(rowSel));
|
|
362
|
+
if (!allRows.length)
|
|
363
|
+
return { first: 0, last: 0 };
|
|
364
|
+
// Use a row that's fully within the scroller's viewport bounds.
|
|
365
|
+
// The first DOM row is often an overscan row with extra columns rendered
|
|
366
|
+
// outside the clip boundary, causing the range to appear wider than it is.
|
|
367
|
+
let targetRow = allRows[0];
|
|
368
|
+
if (scroller) {
|
|
369
|
+
const scrollerRect = scroller.getBoundingClientRect();
|
|
370
|
+
const inView = allRows.find(r => {
|
|
371
|
+
const rect = r.getBoundingClientRect();
|
|
372
|
+
return rect.height > 0 &&
|
|
373
|
+
rect.top >= scrollerRect.top &&
|
|
374
|
+
rect.bottom <= scrollerRect.bottom;
|
|
375
|
+
});
|
|
376
|
+
if (inView)
|
|
377
|
+
targetRow = inView;
|
|
378
|
+
}
|
|
379
|
+
const scrollerRect = scroller === null || scroller === void 0 ? void 0 : scroller.getBoundingClientRect();
|
|
380
|
+
const indices = Array.from(targetRow.querySelectorAll(cellSel))
|
|
381
|
+
.map(c => {
|
|
382
|
+
if (scrollerRect) {
|
|
383
|
+
const rect = c.getBoundingClientRect();
|
|
384
|
+
const visible = rect.width > 0 &&
|
|
385
|
+
rect.right > scrollerRect.left &&
|
|
386
|
+
rect.left < scrollerRect.right;
|
|
387
|
+
if (!visible)
|
|
388
|
+
return NaN;
|
|
389
|
+
}
|
|
390
|
+
return Number(c.getAttribute('aria-colindex')) - 1;
|
|
391
|
+
})
|
|
392
|
+
.filter(n => !isNaN(n) && n >= 0);
|
|
393
|
+
if (!indices.length)
|
|
394
|
+
return { first: 0, last: 0 };
|
|
395
|
+
return { first: Math.min(...indices), last: Math.max(...indices) };
|
|
396
|
+
}, { rowSel, cellSel });
|
|
397
|
+
},
|
|
398
|
+
scrollToRow: async ({ root, config }, rowIndex) => {
|
|
399
|
+
const rowSel = config.rowSelector;
|
|
400
|
+
await root.evaluate((el, { rowSel, idx }) => {
|
|
401
|
+
const scroller = el.querySelector('.MuiDataGrid-virtualScroller');
|
|
402
|
+
if (!scroller)
|
|
403
|
+
return;
|
|
404
|
+
const existingRow = el.querySelector(`${rowSel}[data-rowindex="${idx}"]`);
|
|
405
|
+
if (existingRow) {
|
|
406
|
+
// Row is in DOM — adjust scrollTop only, never scrollLeft.
|
|
407
|
+
const scrollerRect = scroller.getBoundingClientRect();
|
|
408
|
+
const rowRect = existingRow.getBoundingClientRect();
|
|
409
|
+
if (rowRect.top < scrollerRect.top) {
|
|
410
|
+
scroller.scrollTop -= (scrollerRect.top - rowRect.top) + 4;
|
|
411
|
+
}
|
|
412
|
+
else if (rowRect.bottom > scrollerRect.bottom) {
|
|
413
|
+
scroller.scrollTop += (rowRect.bottom - scrollerRect.bottom) + 4;
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// Row not in DOM — interpolate from visible rows' style.top positions.
|
|
418
|
+
// This is far more accurate than estimating from row heights because MUI
|
|
419
|
+
// DataGrid uses absolute positioning (style.top) for each virtual row.
|
|
420
|
+
const visibleRows = Array.from(el.querySelectorAll(rowSel));
|
|
421
|
+
const rowsWithPos = visibleRows
|
|
422
|
+
.map(r => {
|
|
423
|
+
const ridx = Number(r.getAttribute('data-rowindex'));
|
|
424
|
+
const top = parseFloat(r.style.top);
|
|
425
|
+
return { ridx, top };
|
|
426
|
+
})
|
|
427
|
+
.filter(r => !isNaN(r.ridx) && !isNaN(r.top) && r.top >= 0)
|
|
428
|
+
.sort((a, b) => a.ridx - b.ridx);
|
|
429
|
+
if (rowsWithPos.length >= 2) {
|
|
430
|
+
const first = rowsWithPos[0];
|
|
431
|
+
const last = rowsWithPos[rowsWithPos.length - 1];
|
|
432
|
+
const rowHeight = (last.top - first.top) / (last.ridx - first.ridx);
|
|
433
|
+
const targetTop = first.top + (idx - first.ridx) * rowHeight;
|
|
434
|
+
scroller.scrollTop = Math.max(0, targetTop - 20);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// Fallback: use bounding-rect heights.
|
|
438
|
+
const heights = visibleRows
|
|
439
|
+
.map(r => r.getBoundingClientRect().height)
|
|
440
|
+
.filter(h => h > 0);
|
|
441
|
+
const estimatedHeight = heights.length
|
|
442
|
+
? heights.reduce((s, h) => s + h, 0) / heights.length
|
|
443
|
+
: 52;
|
|
444
|
+
scroller.scrollTop = Math.max(0, idx * estimatedHeight - 20);
|
|
445
|
+
}, { rowSel, idx: rowIndex });
|
|
446
|
+
// Swallow timeout: the row may still be readable if it re-enters the
|
|
447
|
+
// virtual viewport. targetReached() in the caller is the authoritative check.
|
|
448
|
+
await root.locator(`${rowSel}[data-rowindex="${rowIndex}"]`)
|
|
449
|
+
.waitFor({ state: 'attached', timeout: 3000 })
|
|
450
|
+
.catch(() => { });
|
|
451
|
+
},
|
|
452
|
+
scrollToColumn: async ({ root, config, page }, colIndex) => {
|
|
453
|
+
const headerSel = typeof config.headerSelector === 'string' ? config.headerSelector : null;
|
|
454
|
+
await root.evaluate((el, { headerSel, idx }) => {
|
|
455
|
+
const scroller = el.querySelector('.MuiDataGrid-virtualScroller');
|
|
456
|
+
if (!scroller || !headerSel)
|
|
457
|
+
return;
|
|
458
|
+
const headers = Array.from(el.querySelectorAll(headerSel));
|
|
459
|
+
const targetAriaIdx = idx + 1; // aria-colindex is 1-based
|
|
460
|
+
// Find header by aria-colindex, NOT by DOM position. With column
|
|
461
|
+
// virtualization, DOM order does not match colIndex — only headers in
|
|
462
|
+
// the current virtual window are rendered, so headers[idx] would be wrong.
|
|
463
|
+
const target = headers.find(h => Number(h.getAttribute('aria-colindex')) === targetAriaIdx);
|
|
464
|
+
if (!target) {
|
|
465
|
+
// Header not in DOM (column virtualized away). Interpolate scrollLeft
|
|
466
|
+
// from the positions of currently visible headers.
|
|
467
|
+
const visibleHeaders = headers
|
|
468
|
+
.map(h => ({ colIdx: Number(h.getAttribute('aria-colindex')), rect: h.getBoundingClientRect() }))
|
|
469
|
+
.filter(h => !isNaN(h.colIdx) && h.rect.width > 0)
|
|
470
|
+
.sort((a, b) => a.colIdx - b.colIdx);
|
|
471
|
+
if (visibleHeaders.length >= 2) {
|
|
472
|
+
const first = visibleHeaders[0];
|
|
473
|
+
const last = visibleHeaders[visibleHeaders.length - 1];
|
|
474
|
+
const pxPerCol = (last.rect.left - first.rect.left) / (last.colIdx - first.colIdx);
|
|
475
|
+
const sRect = scroller.getBoundingClientRect();
|
|
476
|
+
const firstAbsLeft = first.rect.left - sRect.left + scroller.scrollLeft;
|
|
477
|
+
scroller.scrollLeft = Math.max(0, firstAbsLeft + (targetAriaIdx - first.colIdx) * pxPerCol - 20);
|
|
478
|
+
}
|
|
479
|
+
else if (visibleHeaders.length === 1) {
|
|
480
|
+
const h = visibleHeaders[0];
|
|
481
|
+
scroller.scrollLeft = Math.max(0, scroller.scrollLeft + (targetAriaIdx - h.colIdx) * h.rect.width - 20);
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const cRect = scroller.getBoundingClientRect();
|
|
486
|
+
const tRect = target.getBoundingClientRect();
|
|
487
|
+
if (tRect.left < cRect.left) {
|
|
488
|
+
scroller.scrollLeft -= (cRect.left - tRect.left) + 20;
|
|
489
|
+
}
|
|
490
|
+
else if (tRect.right > cRect.right) {
|
|
491
|
+
scroller.scrollLeft += (tRect.right - cRect.right) + 20;
|
|
492
|
+
}
|
|
493
|
+
}, { headerSel, idx: colIndex });
|
|
494
|
+
// Wait for any cell with this colIndex to appear. With column virtualization
|
|
495
|
+
// (32 cols), MUI DataGrid updates the DOM on requestAnimationFrame after a
|
|
496
|
+
// scrollLeft change — so we may need to wait a tick. If it doesn't appear,
|
|
497
|
+
// fall back to a brief pause; targetReached() is the authoritative gate.
|
|
498
|
+
// Scope to body cells (.MuiDataGrid-cell) — [aria-colindex] also matches
|
|
499
|
+
// column headers which appear before body cells after a scroll.
|
|
500
|
+
const cellSel = typeof config.cellSelector === 'string' ? config.cellSelector : '.MuiDataGrid-cell';
|
|
501
|
+
const appeared = await root.locator(`${cellSel}[aria-colindex="${colIndex + 1}"]`)
|
|
502
|
+
.first()
|
|
503
|
+
.waitFor({ state: 'attached', timeout: 2000 })
|
|
504
|
+
.then(() => true)
|
|
505
|
+
.catch(() => false);
|
|
506
|
+
if (!appeared) {
|
|
507
|
+
await page.waitForTimeout(300);
|
|
337
508
|
}
|
|
338
|
-
},
|
|
339
|
-
await root.locator(`${config.rowSelector} [aria-colindex="${colIndex + 1}"]`)
|
|
340
|
-
.first()
|
|
341
|
-
.waitFor({ state: 'attached', timeout: 3000 });
|
|
509
|
+
},
|
|
342
510
|
},
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
511
|
+
loading: {
|
|
512
|
+
isTableLoading: async (context) => {
|
|
513
|
+
return await context.root.locator('.MuiDataGrid-overlay').isVisible().catch(() => false);
|
|
514
|
+
}
|
|
347
515
|
}
|
|
348
516
|
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
/** Default preset for MUI DataGrid. For non-English locales use `createMuiDataGrid({ buttonLabels })`. */
|
|
520
|
+
exports.muiDataGrid = createMuiDataGrid();
|