playwright-ag-grid 1.0.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.
- package/CHANGELOG.md +12 -0
- package/README.md +387 -0
- package/package.json +41 -0
- package/playwright.config.js +21 -0
- package/src/index.d.ts +33 -0
- package/src/index.js +393 -0
- package/test-results/.last-run.json +4 -0
- package/tests/ag-grid-animation-wait.v35.spec.js +52 -0
- package/tests/ag-grid-data.v33.spec.js +6 -0
- package/tests/ag-grid-data.v34.spec.js +6 -0
- package/tests/ag-grid-data.v35.spec.js +6 -0
- package/tests/ag-grid-elements.v33.spec.js +6 -0
- package/tests/ag-grid-elements.v34.spec.js +6 -0
- package/tests/ag-grid-elements.v35.spec.js +6 -0
- package/tests/server.mjs +47 -0
- package/tests/shared/fixtures.js +109 -0
- package/tests/shared/run-ag-grid-data-suite.js +598 -0
- package/tests/shared/run-ag-grid-elements-suite.js +49 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import {
|
|
2
|
+
browserExtractAgGrid,
|
|
3
|
+
browserWaitForAgGridAnimation,
|
|
4
|
+
filterOperator,
|
|
5
|
+
filterTab,
|
|
6
|
+
sort,
|
|
7
|
+
} from "@kpmck/ag-grid-core";
|
|
8
|
+
|
|
9
|
+
function escapeRegExp(value) {
|
|
10
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function getHeaderTextLocator(rootLocator, columnName) {
|
|
14
|
+
return rootLocator
|
|
15
|
+
.locator(".ag-header-cell-text")
|
|
16
|
+
.filter({ hasText: new RegExp(`^${escapeRegExp(columnName)}$`) })
|
|
17
|
+
.first();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function getColumnHeaderMeta(rootLocator, columnName) {
|
|
21
|
+
const metadata = await rootLocator.evaluate((root, targetColumnName) => {
|
|
22
|
+
const headerTexts = [...root.querySelectorAll(".ag-header-cell-text")];
|
|
23
|
+
const matchingHeaderText = headerTexts.find(
|
|
24
|
+
(element) => element.textContent.trim() === targetColumnName
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!matchingHeaderText) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const headerCell = matchingHeaderText.closest(".ag-header-cell");
|
|
32
|
+
const visibleHeaderCells = [...root.querySelectorAll(".ag-header-row-column .ag-header-cell")]
|
|
33
|
+
.filter((element) => element.offsetParent !== null);
|
|
34
|
+
const headerPosition = headerCell
|
|
35
|
+
? visibleHeaderCells.indexOf(headerCell)
|
|
36
|
+
: -1;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
columnIndex: headerCell?.getAttribute("aria-colindex") ?? null,
|
|
40
|
+
headerPosition,
|
|
41
|
+
};
|
|
42
|
+
}, columnName);
|
|
43
|
+
|
|
44
|
+
if (!metadata) {
|
|
45
|
+
throw new Error(`Unable to find AG Grid column "${columnName}".`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return metadata;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function getHeaderCellLocator(rootLocator, columnName) {
|
|
52
|
+
const { columnIndex } = await getColumnHeaderMeta(rootLocator, columnName);
|
|
53
|
+
if (!columnIndex) {
|
|
54
|
+
throw new Error(`Unable to resolve a header cell for "${columnName}".`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return rootLocator.locator(`.ag-header-row-column .ag-header-cell[aria-colindex="${columnIndex}"]`).first();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getMenuTabLocator(rootLocator, tabName) {
|
|
61
|
+
return rootLocator.locator(".ag-tab").locator(`.ag-icon-${tabName}`).locator("xpath=ancestor::span[1]").first();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function maybeCloseMenuTab(rootLocator, noMenuTabs = false) {
|
|
65
|
+
if (noMenuTabs) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tabs = rootLocator.locator(".ag-tab");
|
|
70
|
+
if ((await tabs.count()) === 0) {
|
|
71
|
+
await rootLocator.evaluate(browserWaitForAgGridAnimation);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await (await getMenuTabLocator(rootLocator, filterTab.filter)).click();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function getFloatingFilterButton(rootLocator, columnName) {
|
|
79
|
+
const { columnIndex, headerPosition } = await getColumnHeaderMeta(rootLocator, columnName);
|
|
80
|
+
|
|
81
|
+
const usesV35FloatingFilterRow =
|
|
82
|
+
(await rootLocator.locator(".ag-header-row-filter").count()) > 0;
|
|
83
|
+
|
|
84
|
+
let buttonLocator = usesV35FloatingFilterRow
|
|
85
|
+
? rootLocator.locator(
|
|
86
|
+
`.ag-header-row-filter .ag-header-cell[aria-colindex="${columnIndex}"] .ag-floating-filter-button:visible`
|
|
87
|
+
)
|
|
88
|
+
: rootLocator.locator(
|
|
89
|
+
`.ag-header-row-column-filter .ag-header-cell[aria-colindex="${columnIndex}"] .ag-floating-filter-button-button:visible`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if ((await buttonLocator.count()) === 0 && headerPosition > -1) {
|
|
93
|
+
buttonLocator = usesV35FloatingFilterRow
|
|
94
|
+
? rootLocator.locator(".ag-header-row-filter .ag-floating-filter-button:visible").nth(headerPosition)
|
|
95
|
+
: rootLocator
|
|
96
|
+
.locator(".ag-header-row-column-filter .ag-floating-filter-button-button:visible")
|
|
97
|
+
.nth(headerPosition);
|
|
98
|
+
} else {
|
|
99
|
+
buttonLocator = buttonLocator.first();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return buttonLocator;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function getFilterColumnButton(rootLocator, columnName, isFloatingFilter = false) {
|
|
106
|
+
if (isFloatingFilter) {
|
|
107
|
+
return getFloatingFilterButton(rootLocator, columnName);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const headerCell = await getHeaderCellLocator(rootLocator, columnName);
|
|
111
|
+
await headerCell.hover();
|
|
112
|
+
return headerCell.locator(".ag-header-cell-filter-button").first();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function toggleColumnCheckboxFilter(rootLocator, filterValue, doSelect) {
|
|
116
|
+
const label = rootLocator
|
|
117
|
+
.page()
|
|
118
|
+
.locator(".ag-popup .ag-input-field-label:visible")
|
|
119
|
+
.filter({
|
|
120
|
+
hasText: new RegExp(`^${escapeRegExp(filterValue)}$`),
|
|
121
|
+
})
|
|
122
|
+
.first();
|
|
123
|
+
await label.waitFor({ state: "visible" });
|
|
124
|
+
const toggle = label.locator("xpath=following-sibling::div[1]").first();
|
|
125
|
+
const checkbox = label.locator("xpath=following-sibling::div[1]//input").first();
|
|
126
|
+
|
|
127
|
+
const isChecked = await checkbox.isChecked();
|
|
128
|
+
if (isChecked !== doSelect) {
|
|
129
|
+
await toggle.click({ force: true });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function filterBySearchTerm(rootLocator, options) {
|
|
134
|
+
const filterValue = options.searchCriteria.filterValue;
|
|
135
|
+
const operator = options.searchCriteria.operator;
|
|
136
|
+
const searchInputIndex = options.searchCriteria.searchInputIndex || 0;
|
|
137
|
+
const operatorIndex =
|
|
138
|
+
options.searchCriteria.operatorIndex ??
|
|
139
|
+
(operator === filterOperator.inRange ? 0 : searchInputIndex);
|
|
140
|
+
const isMultiFilter = options.searchCriteria.isMultiFilter;
|
|
141
|
+
|
|
142
|
+
if (operator) {
|
|
143
|
+
const picker = rootLocator
|
|
144
|
+
.locator(".ag-filter .ag-picker-field-wrapper:visible")
|
|
145
|
+
.nth(operatorIndex);
|
|
146
|
+
await rootLocator.evaluate(browserWaitForAgGridAnimation);
|
|
147
|
+
await picker.click();
|
|
148
|
+
await rootLocator.locator(".ag-popup .ag-list span").filter({
|
|
149
|
+
hasText: new RegExp(`^${escapeRegExp(operator)}$`),
|
|
150
|
+
}).first().click();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (isMultiFilter) {
|
|
154
|
+
const selectAllText = options.selectAllLocaleText || "(Select All)";
|
|
155
|
+
await toggleColumnCheckboxFilter(rootLocator, selectAllText, false);
|
|
156
|
+
const miniFilterInput = rootLocator
|
|
157
|
+
.page()
|
|
158
|
+
.locator(".ag-popup-child input:not([type='radio']):not([type='checkbox']):visible")
|
|
159
|
+
.first();
|
|
160
|
+
if (await miniFilterInput.count()) {
|
|
161
|
+
await miniFilterInput.fill("");
|
|
162
|
+
await miniFilterInput.type(`${filterValue}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
!isMultiFilter &&
|
|
168
|
+
operator !== filterOperator.blank &&
|
|
169
|
+
operator !== filterOperator.notBlank
|
|
170
|
+
) {
|
|
171
|
+
const input = rootLocator
|
|
172
|
+
.locator(".ag-popup-child input:not([type='radio']):not([type='checkbox']):visible")
|
|
173
|
+
.nth(searchInputIndex);
|
|
174
|
+
await input.fill("");
|
|
175
|
+
await input.type(`${filterValue}`);
|
|
176
|
+
await input.press("Enter");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (isMultiFilter) {
|
|
180
|
+
await toggleColumnCheckboxFilter(rootLocator, filterValue, true);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeFloatingFilterSearchCriteria(searchCriteria) {
|
|
185
|
+
const betweenInputIndexes = new Map();
|
|
186
|
+
|
|
187
|
+
return searchCriteria.map((criteria) => {
|
|
188
|
+
if (
|
|
189
|
+
criteria.operator !== filterOperator.inRange ||
|
|
190
|
+
criteria.searchInputIndex !== undefined
|
|
191
|
+
) {
|
|
192
|
+
return criteria;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const criteriaKey = `${criteria.columnName}::${criteria.operator}`;
|
|
196
|
+
const nextInputIndex = betweenInputIndexes.get(criteriaKey) || 0;
|
|
197
|
+
betweenInputIndexes.set(criteriaKey, nextInputIndex + 1);
|
|
198
|
+
|
|
199
|
+
return { ...criteria, searchInputIndex: nextInputIndex };
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function groupFloatingFilterSearchCriteria(searchCriteria) {
|
|
204
|
+
const groupedCriteria = [];
|
|
205
|
+
|
|
206
|
+
searchCriteria.forEach((criteria) => {
|
|
207
|
+
const lastGroup = groupedCriteria[groupedCriteria.length - 1];
|
|
208
|
+
|
|
209
|
+
if (
|
|
210
|
+
criteria.operator === filterOperator.inRange &&
|
|
211
|
+
lastGroup &&
|
|
212
|
+
lastGroup[0].columnName === criteria.columnName &&
|
|
213
|
+
lastGroup[0].operator === criteria.operator
|
|
214
|
+
) {
|
|
215
|
+
lastGroup.push(criteria);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
groupedCriteria.push([criteria]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return groupedCriteria;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function applyColumnFilter(rootLocator, hasApplyButton, noMenuTabs) {
|
|
226
|
+
if (hasApplyButton) {
|
|
227
|
+
await rootLocator.locator(".ag-filter-apply-panel-button").getByText("Apply", { exact: true }).click();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await maybeCloseMenuTab(rootLocator, noMenuTabs);
|
|
231
|
+
await rootLocator.page().keyboard.press("Escape");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export class PlaywrightAgGrid {
|
|
235
|
+
constructor(rootLocator) {
|
|
236
|
+
this.rootLocator = rootLocator;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async waitForAnimation(options = {}) {
|
|
240
|
+
await this.rootLocator.evaluate(browserWaitForAgGridAnimation, options);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async getData(options = {}) {
|
|
244
|
+
await this.rootLocator.evaluate(browserWaitForAgGridAnimation, options);
|
|
245
|
+
return this.rootLocator.evaluate(browserExtractAgGrid, options);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async sortColumn(columnName, sortDirection) {
|
|
249
|
+
let normalized = sortDirection;
|
|
250
|
+
|
|
251
|
+
if (normalized.toLowerCase() === "ascending") {
|
|
252
|
+
normalized = "asc";
|
|
253
|
+
} else if (normalized.toLowerCase() === "descending") {
|
|
254
|
+
normalized = "desc";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (normalized !== sort.ascending && normalized !== sort.descending) {
|
|
258
|
+
throw new Error("sortDirection must be either 'asc' or 'desc'.");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const headerText = await getHeaderTextLocator(this.rootLocator, columnName);
|
|
262
|
+
const container = headerText.locator(
|
|
263
|
+
"xpath=ancestor::*[contains(@class, 'ag-cell-label-container')][1]"
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
for (let attempts = 0; attempts < 3; attempts += 1) {
|
|
267
|
+
const className = (await container.getAttribute("class")) || "";
|
|
268
|
+
if (className.includes(`ag-header-cell-sorted-${normalized}`)) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
await headerText.click();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async pinColumn(columnName, pin) {
|
|
276
|
+
const headerCell = await getHeaderCellLocator(this.rootLocator, columnName);
|
|
277
|
+
await headerCell.hover();
|
|
278
|
+
await headerCell.locator(".ag-header-cell-menu-button").click();
|
|
279
|
+
if ((await this.rootLocator.locator(".ag-tab").count()) > 0) {
|
|
280
|
+
await (await getMenuTabLocator(this.rootLocator, filterTab.general)).click();
|
|
281
|
+
}
|
|
282
|
+
await this.rootLocator.locator(".ag-menu-option").getByText("Pin Column", { exact: true }).click();
|
|
283
|
+
|
|
284
|
+
const selectedOption =
|
|
285
|
+
pin === "left" ? "Pin Left" : pin === "right" ? "Pin Right" : "No Pin";
|
|
286
|
+
|
|
287
|
+
await this.rootLocator.locator(".ag-menu-option").getByText(selectedOption, { exact: true }).click();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async filterTextMenu(options) {
|
|
291
|
+
const criteriaList = Array.isArray(options.searchCriteria)
|
|
292
|
+
? options.searchCriteria
|
|
293
|
+
: [options.searchCriteria];
|
|
294
|
+
|
|
295
|
+
for (const searchCriteria of criteriaList) {
|
|
296
|
+
const optionSet = { ...options, searchCriteria };
|
|
297
|
+
await (await getFilterColumnButton(this.rootLocator, searchCriteria.columnName)).click();
|
|
298
|
+
await filterBySearchTerm(this.rootLocator, optionSet);
|
|
299
|
+
await applyColumnFilter(this.rootLocator, options.hasApplyButton, options.noMenuTabs);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async filterTextFloating(options) {
|
|
304
|
+
const criteriaList = Array.isArray(options.searchCriteria)
|
|
305
|
+
? normalizeFloatingFilterSearchCriteria(options.searchCriteria)
|
|
306
|
+
: [options.searchCriteria];
|
|
307
|
+
|
|
308
|
+
const groups = groupFloatingFilterSearchCriteria(criteriaList);
|
|
309
|
+
|
|
310
|
+
for (const group of groups) {
|
|
311
|
+
await (await getFilterColumnButton(this.rootLocator, group[0].columnName, true)).click();
|
|
312
|
+
|
|
313
|
+
for (let index = 0; index < group.length; index += 1) {
|
|
314
|
+
const criteria = group[index];
|
|
315
|
+
await filterBySearchTerm(this.rootLocator, {
|
|
316
|
+
...options,
|
|
317
|
+
searchCriteria: index === 0 ? criteria : { ...criteria, operator: undefined },
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await applyColumnFilter(this.rootLocator, options.hasApplyButton, options.noMenuTabs);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async filterCheckboxMenu(options) {
|
|
326
|
+
const criteriaList = Array.isArray(options.searchCriteria)
|
|
327
|
+
? options.searchCriteria
|
|
328
|
+
: [options.searchCriteria];
|
|
329
|
+
|
|
330
|
+
for (const searchCriteria of criteriaList) {
|
|
331
|
+
await (await getFilterColumnButton(this.rootLocator, searchCriteria.columnName)).click();
|
|
332
|
+
await toggleColumnCheckboxFilter(
|
|
333
|
+
this.rootLocator,
|
|
334
|
+
options.selectAllLocaleText || "(Select All)",
|
|
335
|
+
false
|
|
336
|
+
);
|
|
337
|
+
await toggleColumnCheckboxFilter(this.rootLocator, searchCriteria.filterValue, true);
|
|
338
|
+
await applyColumnFilter(this.rootLocator, options.hasApplyButton, options.noMenuTabs);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async toggleColumnFromSideBar(columnName, doRemove) {
|
|
343
|
+
const columnFilterInput = this.rootLocator.locator(
|
|
344
|
+
".ag-column-select-header-filter-wrapper input"
|
|
345
|
+
).first();
|
|
346
|
+
|
|
347
|
+
if (!(await columnFilterInput.isVisible())) {
|
|
348
|
+
await this.rootLocator.page().locator(".ag-side-buttons span").getByText("Columns", { exact: true }).click();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await this.waitForAnimation();
|
|
352
|
+
await columnFilterInput.fill(columnName);
|
|
353
|
+
const checkbox = this.rootLocator.page()
|
|
354
|
+
.locator(".ag-column-select-column-label")
|
|
355
|
+
.filter({ hasText: new RegExp(`^${escapeRegExp(columnName)}$`) })
|
|
356
|
+
.first()
|
|
357
|
+
.locator("xpath=ancestor::*[1]//input")
|
|
358
|
+
.first();
|
|
359
|
+
|
|
360
|
+
if (doRemove) {
|
|
361
|
+
await checkbox.uncheck({ force: true });
|
|
362
|
+
} else {
|
|
363
|
+
await checkbox.check({ force: true });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async getCellLocator(rowMatcher, columnName) {
|
|
368
|
+
const rows = await this.getData();
|
|
369
|
+
const rowIndex = rows.findIndex((row) =>
|
|
370
|
+
Object.entries(rowMatcher).every(([key, value]) => row[key] === value)
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (rowIndex === -1) {
|
|
374
|
+
throw new Error(`Unable to find row matching ${JSON.stringify(rowMatcher)}.`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const valuesArray = await this.getData({ valuesArray: true });
|
|
378
|
+
const columnIndex = valuesArray.headers.indexOf(columnName);
|
|
379
|
+
|
|
380
|
+
if (columnIndex === -1) {
|
|
381
|
+
throw new Error(`Unable to find column "${columnName}".`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const visibleRows = this.rootLocator.locator(".ag-center-cols-clipper .ag-row:not(.ag-opacity-zero), .ag-center-cols-viewport .ag-row:not(.ag-opacity-zero)");
|
|
385
|
+
return visibleRows.nth(rowIndex).locator(".ag-cell").nth(columnIndex);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function createAgGrid(rootLocator) {
|
|
390
|
+
return new PlaywrightAgGrid(rootLocator);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export { filterOperator, sort };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import { createAgGrid } from "../src/index.js";
|
|
4
|
+
|
|
5
|
+
test.describe("agGridWaitForAnimation", () => {
|
|
6
|
+
test("waits for AG Grid-owned animations to finish", async ({ page }) => {
|
|
7
|
+
await page.goto("/animation-wait/ag-owned.html");
|
|
8
|
+
await page.locator(".ag-cell").first().waitFor({ state: "visible" });
|
|
9
|
+
|
|
10
|
+
await page.evaluate(() => {
|
|
11
|
+
window.startAnimationWaitScenario();
|
|
12
|
+
window.__animationProbe.waitStartedAt = Date.now();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const grid = createAgGrid(page.locator("#myGrid"));
|
|
16
|
+
await grid.waitForAnimation();
|
|
17
|
+
|
|
18
|
+
const probe = await page.evaluate(() => ({
|
|
19
|
+
...window.__animationProbe,
|
|
20
|
+
elapsedMs: Date.now() - window.__animationProbe.waitStartedAt,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
expect(probe.agStarted).toBe(true);
|
|
24
|
+
expect(probe.agFinished).toBe(true);
|
|
25
|
+
expect(probe.elapsedMs).toBeGreaterThan(200);
|
|
26
|
+
expect(probe.elapsedMs).toBeLessThan(2000);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("ignores third-party subtree animations whose finished promise never resolves", async ({ page }) => {
|
|
30
|
+
await page.goto("/animation-wait/third-party-subtree.html");
|
|
31
|
+
await page.locator(".ag-cell").first().waitFor({ state: "visible" });
|
|
32
|
+
|
|
33
|
+
await page.evaluate(() => {
|
|
34
|
+
window.startAnimationWaitScenario();
|
|
35
|
+
window.__animationProbe.waitStartedAt = Date.now();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(await page.locator("#myGrid .os-scrollbar-handle").count()).toBeGreaterThan(0);
|
|
39
|
+
|
|
40
|
+
const grid = createAgGrid(page.locator("#myGrid"));
|
|
41
|
+
await grid.waitForAnimation();
|
|
42
|
+
|
|
43
|
+
const probe = await page.evaluate(() => ({
|
|
44
|
+
...window.__animationProbe,
|
|
45
|
+
elapsedMs: Date.now() - window.__animationProbe.waitStartedAt,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
expect(probe.agFinished).toBe(true);
|
|
49
|
+
expect(probe.thirdPartyInstalled).toBe(true);
|
|
50
|
+
expect(probe.elapsedMs).toBeLessThan(2000);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/tests/server.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const appRoot = path.resolve(__dirname, "../../cypress-ag-grid/app");
|
|
9
|
+
const port = 4173;
|
|
10
|
+
|
|
11
|
+
const contentTypes = {
|
|
12
|
+
".css": "text/css; charset=utf-8",
|
|
13
|
+
".html": "text/html; charset=utf-8",
|
|
14
|
+
".js": "application/javascript; charset=utf-8",
|
|
15
|
+
".json": "application/json; charset=utf-8",
|
|
16
|
+
".png": "image/png",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
http
|
|
20
|
+
.createServer((req, res) => {
|
|
21
|
+
const requestPath = req.url === "/" ? "/index.html" : req.url;
|
|
22
|
+
const safePath = path.normalize(decodeURIComponent(requestPath)).replace(/^(\.\.[/\\])+/, "");
|
|
23
|
+
const filePath = path.join(appRoot, safePath);
|
|
24
|
+
|
|
25
|
+
if (!filePath.startsWith(appRoot)) {
|
|
26
|
+
res.writeHead(403);
|
|
27
|
+
res.end("Forbidden");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fs.readFile(filePath, (error, data) => {
|
|
32
|
+
if (error) {
|
|
33
|
+
res.writeHead(error.code === "ENOENT" ? 404 : 500);
|
|
34
|
+
res.end(error.code === "ENOENT" ? "Not found" : "Server error");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
res.writeHead(200, {
|
|
39
|
+
"Content-Type":
|
|
40
|
+
contentTypes[path.extname(filePath)] || "application/octet-stream",
|
|
41
|
+
});
|
|
42
|
+
res.end(data);
|
|
43
|
+
});
|
|
44
|
+
})
|
|
45
|
+
.listen(port, "127.0.0.1", () => {
|
|
46
|
+
console.log(`Playwright AG Grid test server listening on ${port}`);
|
|
47
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const cardataPath = path.resolve(
|
|
8
|
+
__dirname,
|
|
9
|
+
"../../../cypress-ag-grid/cypress/fixtures/cardata.json"
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export const agGridSelector = "#myGrid";
|
|
13
|
+
export const agGridElementsSelector = "#myGrid2";
|
|
14
|
+
|
|
15
|
+
export const pageSize = 5;
|
|
16
|
+
|
|
17
|
+
export const cardataFixture = JSON.parse(
|
|
18
|
+
fs.readFileSync(cardataPath, "utf-8")
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const expectedPaginatedTableData = [
|
|
22
|
+
[
|
|
23
|
+
{ Year: "2020", Make: "Toyota", Model: "Celica", Condition: "fair", Price: "35000" },
|
|
24
|
+
{ Year: "2020", Make: "Ford", Model: "Mondeo", Condition: "excellent", Price: "32000" },
|
|
25
|
+
{ Year: "2020", Make: "Porsche", Model: "Boxter", Condition: "good", Price: "72000" },
|
|
26
|
+
{ Year: "2020", Make: "BMW", Model: "3-series", Condition: "fair", Price: "45000" },
|
|
27
|
+
{ Year: "2020", Make: "Mercedes", Model: "GLC300", Condition: "good", Price: "53000" },
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
{ Year: "2020", Make: "Honda", Model: "Civic", Condition: "poor", Price: "22000" },
|
|
31
|
+
{ Year: "2020", Make: "Honda", Model: "Accord", Condition: "poor", Price: "32000" },
|
|
32
|
+
{ Year: "2020", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "19000" },
|
|
33
|
+
{ Year: "2020", Make: "Hyundai", Model: "Elantra", Condition: "good", Price: "22000" },
|
|
34
|
+
{ Year: "2020", Make: "Toyota", Model: "Celica", Condition: "poor", Price: "5000" },
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
{ Year: "2020", Make: "Ford", Model: "Mondeo", Condition: "good", Price: "25000" },
|
|
38
|
+
{ Year: "2020", Make: "Porsche", Model: "Boxter", Condition: "good", Price: "99000" },
|
|
39
|
+
{ Year: "2020", Make: "BMW", Model: "3-series", Condition: "poor", Price: "32000" },
|
|
40
|
+
{ Year: "2020", Make: "Mercedes", Model: "GLC300", Condition: "excellent", Price: "35000" },
|
|
41
|
+
{ Year: "2011", Make: "Honda", Model: "Civic", Condition: "good", Price: "9000" },
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
{ Year: "2020", Make: "Honda", Model: "Accord", Condition: "good", Price: "34000" },
|
|
45
|
+
{ Year: "1990", Make: "Ford", Model: "Taurus", Condition: "excellent", Price: "900" },
|
|
46
|
+
{ Year: "2020", Make: "Hyundai", Model: "Elantra", Condition: "fair", Price: "3000" },
|
|
47
|
+
{ Year: "2020", Make: "BMW", Model: "2002", Condition: "excellent", Price: "88001" },
|
|
48
|
+
{ Year: "2023", Make: "Hyundai", Model: "Santa Fe", Condition: "excellent", Price: "" },
|
|
49
|
+
],
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export const expectedFirstPageTableData = expectedPaginatedTableData[0];
|
|
53
|
+
|
|
54
|
+
export const expectedPorscheRowsBeforeEditing = [
|
|
55
|
+
{ Year: "2020", Make: "Porsche", Model: "Boxter", Price: "72000" },
|
|
56
|
+
{ Year: "2020", Make: "Porsche", Model: "Boxter", Price: "99000" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export const expectedPorscheRowsAfterEditing = [
|
|
60
|
+
{ Year: "2020", Make: "Porsche", Model: "Boxter", Price: "66000" },
|
|
61
|
+
{ Year: "2020", Make: "Porsche", Model: "Boxter", Price: "99000" },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
export function clone(value) {
|
|
65
|
+
return structuredClone(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function removePropertyFromCollection(collection, columnsToExclude) {
|
|
69
|
+
const cloned = clone(collection);
|
|
70
|
+
|
|
71
|
+
if (!columnsToExclude) {
|
|
72
|
+
return cloned;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const excludedColumn of columnsToExclude) {
|
|
76
|
+
for (const row of cloned) {
|
|
77
|
+
delete row[excludedColumn];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return cloned;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function sortedCollectionByProperty(collection, columnName, sortedBy, pageSizeLimit = pageSize) {
|
|
85
|
+
const cloned = clone(collection);
|
|
86
|
+
const direction = sortedBy === "desc" ? -1 : 1;
|
|
87
|
+
|
|
88
|
+
return cloned
|
|
89
|
+
.sort((a, b) => {
|
|
90
|
+
const valueA = String(a[columnName] ?? "").toUpperCase();
|
|
91
|
+
const valueB = String(b[columnName] ?? "").toUpperCase();
|
|
92
|
+
if (valueA < valueB) return -1 * direction;
|
|
93
|
+
if (valueA > valueB) return 1 * direction;
|
|
94
|
+
return 0;
|
|
95
|
+
})
|
|
96
|
+
.slice(0, pageSizeLimit);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getSortedMileage(actualTableData) {
|
|
100
|
+
return actualTableData
|
|
101
|
+
.map((row) => row.Mileage)
|
|
102
|
+
.sort((a, b) => Number(a) - Number(b));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function expectRowsSubset(expect, actualRows, expectedRows) {
|
|
106
|
+
for (const expectedRow of expectedRows) {
|
|
107
|
+
expect(actualRows).toContainEqual(expectedRow);
|
|
108
|
+
}
|
|
109
|
+
}
|