@kpmck/ag-grid-core 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 +7 -0
- package/README.md +11 -0
- package/package.json +31 -0
- package/src/index.d.ts +44 -0
- package/src/index.js +452 -0
- package/src/index.test.js +203 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @kpmck/ag-grid-core
|
|
2
|
+
|
|
3
|
+
Framework-agnostic AG Grid helpers shared by the adapter packages in this monorepo.
|
|
4
|
+
|
|
5
|
+
Planned responsibilities:
|
|
6
|
+
|
|
7
|
+
- DOM traversal and extraction
|
|
8
|
+
- AG Grid animation waiting helpers
|
|
9
|
+
- Shared types and selectors
|
|
10
|
+
|
|
11
|
+
Framework-specific command registration and assertions stay in adapter packages such as `cypress-ag-grid` and `playwright-ag-grid`.
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kpmck/ag-grid-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Framework-agnostic AG Grid DOM helpers for Node-based test tooling",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/kpmck/cypress-ag-grid.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"ag-grid",
|
|
23
|
+
"aggrid",
|
|
24
|
+
"testing",
|
|
25
|
+
"dom",
|
|
26
|
+
"playwright",
|
|
27
|
+
"cypress"
|
|
28
|
+
],
|
|
29
|
+
"author": "Kerry McKeever <kerry@kerrymckeever.com>",
|
|
30
|
+
"license": "MIT"
|
|
31
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface AgGridExtractionOptions {
|
|
2
|
+
onlyColumns?: string[];
|
|
3
|
+
valuesArray?: boolean;
|
|
4
|
+
returnElements?: boolean;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export declare const filterOperator: Record<string, string>;
|
|
9
|
+
export declare const sort: Record<string, string>;
|
|
10
|
+
export declare const filterTab: Record<string, string>;
|
|
11
|
+
|
|
12
|
+
export declare function isRowNotDestroyed(rowElement: Element): boolean;
|
|
13
|
+
|
|
14
|
+
export declare function waitForAgGridAnimation(
|
|
15
|
+
agGridRootElement: Element,
|
|
16
|
+
options?: AgGridExtractionOptions
|
|
17
|
+
): Promise<void>;
|
|
18
|
+
|
|
19
|
+
export declare function getAgGridHeaders(tableElement: Element): string[];
|
|
20
|
+
|
|
21
|
+
export declare function extractAgGrid(
|
|
22
|
+
agGridRootElement: Element,
|
|
23
|
+
options?: AgGridExtractionOptions
|
|
24
|
+
): Array<Record<string, string | Element>> | { headers: string[]; rows: Array<Array<string | Element>> };
|
|
25
|
+
|
|
26
|
+
export declare function extractAgGridData(
|
|
27
|
+
agGridRootElement: Element,
|
|
28
|
+
options?: AgGridExtractionOptions
|
|
29
|
+
): Promise<Array<Record<string, string>> | { headers: string[]; rows: string[][] }>;
|
|
30
|
+
|
|
31
|
+
export declare function extractAgGridElements(
|
|
32
|
+
agGridRootElement: Element,
|
|
33
|
+
options?: AgGridExtractionOptions
|
|
34
|
+
): Promise<Array<Record<string, Element>> | { headers: string[]; rows: Element[][] }>;
|
|
35
|
+
|
|
36
|
+
export declare function browserExtractAgGrid(
|
|
37
|
+
agGridRootElement: Element,
|
|
38
|
+
options?: AgGridExtractionOptions
|
|
39
|
+
): Array<Record<string, string>> | { headers: string[]; rows: string[][] };
|
|
40
|
+
|
|
41
|
+
export declare function browserWaitForAgGridAnimation(
|
|
42
|
+
agGridRootElement: Element,
|
|
43
|
+
options?: AgGridExtractionOptions
|
|
44
|
+
): Promise<void>;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
const AG_GRID_COLUMN_SELECTORS = [
|
|
2
|
+
".ag-pinned-left-cols-container",
|
|
3
|
+
".ag-center-cols-clipper",
|
|
4
|
+
".ag-center-cols-viewport",
|
|
5
|
+
".ag-pinned-right-cols-container",
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
const DEFAULT_ANIMATION_TIMEOUT_MS = 5000;
|
|
9
|
+
|
|
10
|
+
export const filterOperator = {
|
|
11
|
+
contains: "Contains",
|
|
12
|
+
notContains: "Does not contain",
|
|
13
|
+
equals: "Equals",
|
|
14
|
+
notEquals: "Does not equal",
|
|
15
|
+
startsWith: "Begins with",
|
|
16
|
+
endsWith: "Ends with",
|
|
17
|
+
lessThan: "Less than",
|
|
18
|
+
lessThanOrEquals: "Less than or equal to",
|
|
19
|
+
greaterThan: "Greater than",
|
|
20
|
+
greaterThanOrEquals: "Greater than or equal to",
|
|
21
|
+
inRange: "Between",
|
|
22
|
+
blank: "Blank",
|
|
23
|
+
notBlank: "Not blank",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const sort = {
|
|
27
|
+
ascending: "asc",
|
|
28
|
+
descending: "desc",
|
|
29
|
+
none: "none",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const filterTab = {
|
|
33
|
+
columns: "columns",
|
|
34
|
+
filter: "filter",
|
|
35
|
+
search: "search",
|
|
36
|
+
general: "menu",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function isElementNode(value) {
|
|
40
|
+
return Boolean(value && value.nodeType === 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getAttributeValue(element, attribute) {
|
|
44
|
+
const attributeNode = element?.attributes?.[attribute];
|
|
45
|
+
|
|
46
|
+
if (!attributeNode) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof attributeNode.value === "string") {
|
|
51
|
+
return attributeNode.value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof attributeNode.nodeValue === "string") {
|
|
55
|
+
return attributeNode.nodeValue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sortElementsByAttributeValue(attribute) {
|
|
62
|
+
return (a, b) => {
|
|
63
|
+
const contentA = parseInt(getAttributeValue(a, attribute), 10).valueOf();
|
|
64
|
+
const contentB = parseInt(getAttributeValue(b, attribute), 10).valueOf();
|
|
65
|
+
return contentA < contentB ? -1 : contentA > contentB ? 1 : 0;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getTrimmedTextContent(element) {
|
|
70
|
+
return element?.textContent?.trim?.() ?? "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isRowNotDestroyed(rowElement) {
|
|
74
|
+
const rect = rowElement.getBoundingClientRect();
|
|
75
|
+
const viewPortRect = rowElement.parentElement.getBoundingClientRect();
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
rect.top >= viewPortRect.top &&
|
|
79
|
+
rect.left >= viewPortRect.left &&
|
|
80
|
+
rect.bottom <= viewPortRect.bottom &&
|
|
81
|
+
rect.right <= viewPortRect.right
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function waitForAgGridAnimation(
|
|
86
|
+
agGridRootElement,
|
|
87
|
+
options = {}
|
|
88
|
+
) {
|
|
89
|
+
if (!isElementNode(agGridRootElement)) {
|
|
90
|
+
throw new Error(`Couldn't find a valid AG Grid root element.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_ANIMATION_TIMEOUT_MS;
|
|
94
|
+
const animations = agGridRootElement.getAnimations?.({ subtree: true }) ?? [];
|
|
95
|
+
|
|
96
|
+
const agGridAnimations = animations.filter((animation) => {
|
|
97
|
+
const animationTarget = animation.effect?.target;
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
!isElementNode(animationTarget) ||
|
|
101
|
+
!animationTarget.classList
|
|
102
|
+
) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const hasAgGridClass = [...animationTarget.classList].some((className) =>
|
|
107
|
+
className.startsWith("ag-")
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return animationTarget === agGridRootElement || hasAgGridClass;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const finiteAnimations = agGridAnimations.filter((animation) => {
|
|
114
|
+
const iterations = animation.effect?.getTiming?.()?.iterations;
|
|
115
|
+
return iterations !== Infinity;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await Promise.race([
|
|
119
|
+
Promise.all(
|
|
120
|
+
finiteAnimations.map(async (animation) => {
|
|
121
|
+
try {
|
|
122
|
+
await animation.finished;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.name === "AbortError") {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
),
|
|
132
|
+
new Promise((resolve) => {
|
|
133
|
+
setTimeout(resolve, timeoutMs);
|
|
134
|
+
}),
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getAgGridHeaders(tableElement) {
|
|
139
|
+
return [
|
|
140
|
+
...tableElement.querySelectorAll(".ag-header-row-column [aria-colindex]"),
|
|
141
|
+
]
|
|
142
|
+
.sort(sortElementsByAttributeValue("aria-colindex"))
|
|
143
|
+
.map((headerElement) => {
|
|
144
|
+
const headerCells = [
|
|
145
|
+
...headerElement.querySelectorAll(".ag-header-cell-text"),
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
if (headerCells.length === 0) {
|
|
149
|
+
return [getTrimmedTextContent(headerElement)];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return headerCells.map((element) => getTrimmedTextContent(element));
|
|
153
|
+
})
|
|
154
|
+
.flat();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getRowCells(rowElement) {
|
|
158
|
+
const rowCells = [...rowElement.querySelectorAll(".ag-cell[aria-colindex]")];
|
|
159
|
+
|
|
160
|
+
if (rowCells.length > 0) {
|
|
161
|
+
return rowCells;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return [...rowElement.querySelectorAll(".ag-cell")];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getStructuredRows(allRows, returnElements) {
|
|
168
|
+
if (!allRows.length) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return allRows
|
|
173
|
+
.filter((rowCells) => rowCells.length)
|
|
174
|
+
.map((rowCells) =>
|
|
175
|
+
rowCells
|
|
176
|
+
.sort(sortElementsByAttributeValue("aria-colindex"))
|
|
177
|
+
.map((element) =>
|
|
178
|
+
returnElements ? element : getTrimmedTextContent(element)
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function mapRowsToObjects(headers, rows, options = {}) {
|
|
184
|
+
return rows.map((row) =>
|
|
185
|
+
row.reduce((acc, curr, idx) => {
|
|
186
|
+
if (
|
|
187
|
+
(options.onlyColumns && !options.onlyColumns.includes(headers[idx])) ||
|
|
188
|
+
headers[idx] === undefined
|
|
189
|
+
) {
|
|
190
|
+
return acc;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { ...acc, [headers[idx]]: curr };
|
|
194
|
+
}, {})
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function extractAgGrid(tableRootElement, options = {}) {
|
|
199
|
+
if (!isElementNode(tableRootElement)) {
|
|
200
|
+
throw new Error(`Couldn't find a valid AG Grid element.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const returnElements = options.returnElements ?? false;
|
|
204
|
+
const tableElement = tableRootElement.querySelectorAll(".ag-root")[0];
|
|
205
|
+
|
|
206
|
+
if (!tableElement) {
|
|
207
|
+
throw new Error("The provided element does not contain an .ag-root node.");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const headers = getAgGridHeaders(tableElement);
|
|
211
|
+
let allRows = [];
|
|
212
|
+
|
|
213
|
+
AG_GRID_COLUMN_SELECTORS.forEach((selector) => {
|
|
214
|
+
[
|
|
215
|
+
...tableElement.querySelectorAll(
|
|
216
|
+
`${selector}:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)`
|
|
217
|
+
),
|
|
218
|
+
]
|
|
219
|
+
.filter(isRowNotDestroyed)
|
|
220
|
+
.sort(sortElementsByAttributeValue("row-index"))
|
|
221
|
+
.forEach((rowElement) => {
|
|
222
|
+
const rowCells = getRowCells(rowElement);
|
|
223
|
+
const rowIndex = parseInt(getAttributeValue(rowElement, "row-index"), 10);
|
|
224
|
+
|
|
225
|
+
if (allRows[rowIndex]) {
|
|
226
|
+
allRows[rowIndex] = [...allRows[rowIndex], ...rowCells];
|
|
227
|
+
} else {
|
|
228
|
+
allRows[rowIndex] = rowCells;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
allRows = allRows
|
|
234
|
+
.filter((row) => row.length)
|
|
235
|
+
.map((row) => row.filter((cell, index) => row.indexOf(cell) === index));
|
|
236
|
+
|
|
237
|
+
const rows = getStructuredRows(allRows, returnElements);
|
|
238
|
+
|
|
239
|
+
if (options.valuesArray) {
|
|
240
|
+
return { headers, rows };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return mapRowsToObjects(headers, rows, options);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function extractAgGridData(agGridRootElement, options = {}) {
|
|
247
|
+
await waitForAgGridAnimation(agGridRootElement, options);
|
|
248
|
+
return extractAgGrid(agGridRootElement, options);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function extractAgGridElements(agGridRootElement, options = {}) {
|
|
252
|
+
await waitForAgGridAnimation(agGridRootElement, options);
|
|
253
|
+
return extractAgGrid(agGridRootElement, { ...options, returnElements: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Self-contained exports that can be serialized into a browser context by Playwright.
|
|
257
|
+
export async function browserWaitForAgGridAnimation(
|
|
258
|
+
agGridRootElement,
|
|
259
|
+
options = {}
|
|
260
|
+
) {
|
|
261
|
+
function isElementNode(value) {
|
|
262
|
+
return Boolean(value && value.nodeType === 1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!isElementNode(agGridRootElement)) {
|
|
266
|
+
throw new Error(`Couldn't find a valid AG Grid root element.`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
270
|
+
const animations = agGridRootElement.getAnimations?.({ subtree: true }) ?? [];
|
|
271
|
+
|
|
272
|
+
const agGridAnimations = animations.filter((animation) => {
|
|
273
|
+
const animationTarget = animation.effect?.target;
|
|
274
|
+
|
|
275
|
+
if (!isElementNode(animationTarget) || !animationTarget.classList) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const hasAgGridClass = [...animationTarget.classList].some((className) =>
|
|
280
|
+
className.startsWith("ag-")
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return animationTarget === agGridRootElement || hasAgGridClass;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const finiteAnimations = agGridAnimations.filter((animation) => {
|
|
287
|
+
const iterations = animation.effect?.getTiming?.()?.iterations;
|
|
288
|
+
return iterations !== Infinity;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await Promise.race([
|
|
292
|
+
Promise.all(
|
|
293
|
+
finiteAnimations.map(async (animation) => {
|
|
294
|
+
try {
|
|
295
|
+
await animation.finished;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error?.name === "AbortError") {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
),
|
|
305
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
306
|
+
]);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function browserExtractAgGrid(agGridRootElement, options = {}) {
|
|
310
|
+
const agGridColumnSelectors = [
|
|
311
|
+
".ag-pinned-left-cols-container",
|
|
312
|
+
".ag-center-cols-clipper",
|
|
313
|
+
".ag-center-cols-viewport",
|
|
314
|
+
".ag-pinned-right-cols-container",
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
function isElementNode(value) {
|
|
318
|
+
return Boolean(value && value.nodeType === 1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getAttributeValue(element, attribute) {
|
|
322
|
+
const attributeNode = element?.attributes?.[attribute];
|
|
323
|
+
|
|
324
|
+
if (!attributeNode) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (typeof attributeNode.value === "string") {
|
|
329
|
+
return attributeNode.value;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (typeof attributeNode.nodeValue === "string") {
|
|
333
|
+
return attributeNode.nodeValue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function sortElementsByAttributeValue(attribute) {
|
|
340
|
+
return (a, b) => {
|
|
341
|
+
const contentA = parseInt(getAttributeValue(a, attribute), 10).valueOf();
|
|
342
|
+
const contentB = parseInt(getAttributeValue(b, attribute), 10).valueOf();
|
|
343
|
+
return contentA < contentB ? -1 : contentA > contentB ? 1 : 0;
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function getTrimmedTextContent(element) {
|
|
348
|
+
return element?.textContent?.trim?.() ?? "";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function isRowNotDestroyedLocal(rowElement) {
|
|
352
|
+
const rect = rowElement.getBoundingClientRect();
|
|
353
|
+
const viewPortRect = rowElement.parentElement.getBoundingClientRect();
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
rect.top >= viewPortRect.top &&
|
|
357
|
+
rect.left >= viewPortRect.left &&
|
|
358
|
+
rect.bottom <= viewPortRect.bottom &&
|
|
359
|
+
rect.right <= viewPortRect.right
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function getRowCells(rowElement) {
|
|
364
|
+
const rowCells = [...rowElement.querySelectorAll(".ag-cell[aria-colindex]")];
|
|
365
|
+
if (rowCells.length > 0) {
|
|
366
|
+
return rowCells;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return [...rowElement.querySelectorAll(".ag-cell")];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!isElementNode(agGridRootElement)) {
|
|
373
|
+
throw new Error(`Couldn't find a valid AG Grid element.`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const returnElements = options.returnElements ?? false;
|
|
377
|
+
const tableElement = agGridRootElement.querySelectorAll(".ag-root")[0];
|
|
378
|
+
|
|
379
|
+
if (!tableElement) {
|
|
380
|
+
throw new Error("The provided element does not contain an .ag-root node.");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const headers = [
|
|
384
|
+
...tableElement.querySelectorAll(".ag-header-row-column [aria-colindex]"),
|
|
385
|
+
]
|
|
386
|
+
.sort(sortElementsByAttributeValue("aria-colindex"))
|
|
387
|
+
.map((headerElement) => {
|
|
388
|
+
const headerCells = [
|
|
389
|
+
...headerElement.querySelectorAll(".ag-header-cell-text"),
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
if (headerCells.length === 0) {
|
|
393
|
+
return [getTrimmedTextContent(headerElement)];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return headerCells.map((element) => getTrimmedTextContent(element));
|
|
397
|
+
})
|
|
398
|
+
.flat();
|
|
399
|
+
|
|
400
|
+
let allRows = [];
|
|
401
|
+
|
|
402
|
+
agGridColumnSelectors.forEach((selector) => {
|
|
403
|
+
[
|
|
404
|
+
...tableElement.querySelectorAll(
|
|
405
|
+
`${selector}:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)`
|
|
406
|
+
),
|
|
407
|
+
]
|
|
408
|
+
.filter(isRowNotDestroyedLocal)
|
|
409
|
+
.sort(sortElementsByAttributeValue("row-index"))
|
|
410
|
+
.forEach((rowElement) => {
|
|
411
|
+
const rowCells = getRowCells(rowElement);
|
|
412
|
+
const rowIndex = parseInt(getAttributeValue(rowElement, "row-index"), 10);
|
|
413
|
+
|
|
414
|
+
if (allRows[rowIndex]) {
|
|
415
|
+
allRows[rowIndex] = [...allRows[rowIndex], ...rowCells];
|
|
416
|
+
} else {
|
|
417
|
+
allRows[rowIndex] = rowCells;
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
allRows = allRows
|
|
423
|
+
.filter((row) => row.length)
|
|
424
|
+
.map((row) => row.filter((cell, index) => row.indexOf(cell) === index));
|
|
425
|
+
|
|
426
|
+
const rows = allRows
|
|
427
|
+
.filter((rowCells) => rowCells.length)
|
|
428
|
+
.map((rowCells) =>
|
|
429
|
+
rowCells
|
|
430
|
+
.sort(sortElementsByAttributeValue("aria-colindex"))
|
|
431
|
+
.map((element) =>
|
|
432
|
+
returnElements ? getAttributeValue(element, "col-id") ?? getTrimmedTextContent(element) : getTrimmedTextContent(element)
|
|
433
|
+
)
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
if (options.valuesArray) {
|
|
437
|
+
return { headers, rows };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return rows.map((row) =>
|
|
441
|
+
row.reduce((acc, curr, idx) => {
|
|
442
|
+
if (
|
|
443
|
+
(options.onlyColumns && !options.onlyColumns.includes(headers[idx])) ||
|
|
444
|
+
headers[idx] === undefined
|
|
445
|
+
) {
|
|
446
|
+
return acc;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return { ...acc, [headers[idx]]: curr };
|
|
450
|
+
}, {})
|
|
451
|
+
);
|
|
452
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
extractAgGridData,
|
|
6
|
+
extractAgGridElements,
|
|
7
|
+
waitForAgGridAnimation,
|
|
8
|
+
} from "./index.js";
|
|
9
|
+
|
|
10
|
+
function createAttributes(attributes = {}) {
|
|
11
|
+
return Object.fromEntries(
|
|
12
|
+
Object.entries(attributes).map(([key, value]) => [
|
|
13
|
+
key,
|
|
14
|
+
{ nodeValue: String(value), value: String(value) },
|
|
15
|
+
])
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class FakeElement {
|
|
20
|
+
constructor({
|
|
21
|
+
textContent = "",
|
|
22
|
+
attributes = {},
|
|
23
|
+
selectorMap = {},
|
|
24
|
+
rect = { top: 0, left: 0, bottom: 10, right: 10 },
|
|
25
|
+
classList = [],
|
|
26
|
+
nodeType = 1,
|
|
27
|
+
} = {}) {
|
|
28
|
+
this.textContent = textContent;
|
|
29
|
+
this.attributes = createAttributes(attributes);
|
|
30
|
+
this.selectorMap = selectorMap;
|
|
31
|
+
this._rect = rect;
|
|
32
|
+
this.classList = classList;
|
|
33
|
+
this.nodeType = nodeType;
|
|
34
|
+
this.parentElement = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
querySelectorAll(selector) {
|
|
38
|
+
return this.selectorMap[selector] ?? [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getBoundingClientRect() {
|
|
42
|
+
return this._rect;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setSelectorMap(selectorMap) {
|
|
46
|
+
this.selectorMap = selectorMap;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function connect(parent, children) {
|
|
52
|
+
children.forEach((child) => {
|
|
53
|
+
child.parentElement = parent;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createCell(colIndex, text) {
|
|
58
|
+
return new FakeElement({
|
|
59
|
+
textContent: text,
|
|
60
|
+
attributes: { "aria-colindex": colIndex },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createRow(rowIndex, cells, rect = { top: 0, left: 0, bottom: 10, right: 10 }) {
|
|
65
|
+
const row = new FakeElement({
|
|
66
|
+
attributes: { "row-index": rowIndex },
|
|
67
|
+
rect,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
connect(row, cells);
|
|
71
|
+
row.setSelectorMap({
|
|
72
|
+
".ag-cell[aria-colindex]": cells,
|
|
73
|
+
".ag-cell": cells,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const viewport = new FakeElement({
|
|
77
|
+
rect: { top: 0, left: 0, bottom: 100, right: 100 },
|
|
78
|
+
});
|
|
79
|
+
row.parentElement = viewport;
|
|
80
|
+
|
|
81
|
+
return row;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createHeader(colIndex, text) {
|
|
85
|
+
const textElement = new FakeElement({ textContent: text });
|
|
86
|
+
const header = new FakeElement({
|
|
87
|
+
attributes: { "aria-colindex": colIndex },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
connect(header, [textElement]);
|
|
91
|
+
header.setSelectorMap({
|
|
92
|
+
".ag-header-cell-text": [textElement],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return header;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createGridFixture() {
|
|
99
|
+
const headerYear = createHeader(1, "Year");
|
|
100
|
+
const headerMake = createHeader(2, "Make");
|
|
101
|
+
const headerModel = createHeader(3, "Model");
|
|
102
|
+
|
|
103
|
+
const pinnedLeftRow0 = createRow(0, [createCell(1, "2020")]);
|
|
104
|
+
const centerRow0 = createRow(0, [createCell(2, "Toyota"), createCell(3, "Celica")]);
|
|
105
|
+
const centerRow1 = createRow(1, [createCell(1, "2021"), createCell(2, "Ford"), createCell(3, "Mondeo")]);
|
|
106
|
+
const destroyedRow = createRow(
|
|
107
|
+
2,
|
|
108
|
+
[createCell(1, "9999"), createCell(2, "Ghost"), createCell(3, "Row")],
|
|
109
|
+
{ top: 200, left: 0, bottom: 210, right: 10 }
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const agRoot = new FakeElement();
|
|
113
|
+
agRoot.setSelectorMap({
|
|
114
|
+
".ag-header-row-column [aria-colindex]": [headerModel, headerYear, headerMake],
|
|
115
|
+
".ag-pinned-left-cols-container:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)": [pinnedLeftRow0],
|
|
116
|
+
".ag-center-cols-clipper:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)": [centerRow0, centerRow1, destroyedRow],
|
|
117
|
+
".ag-center-cols-viewport:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)": [],
|
|
118
|
+
".ag-pinned-right-cols-container:not(.ag-hidden) .ag-row:not(.ag-opacity-zero)": [],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const gridRoot = new FakeElement();
|
|
122
|
+
gridRoot.setSelectorMap({
|
|
123
|
+
".ag-root": [agRoot],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return { gridRoot, pinnedLeftRow0, centerRow0 };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
test("extractAgGridData returns structured row data and filters destroyed rows", async () => {
|
|
130
|
+
const { gridRoot } = createGridFixture();
|
|
131
|
+
gridRoot.getAnimations = () => [];
|
|
132
|
+
|
|
133
|
+
const rows = await extractAgGridData(gridRoot);
|
|
134
|
+
|
|
135
|
+
assert.deepEqual(rows, [
|
|
136
|
+
{ Year: "2020", Make: "Toyota", Model: "Celica" },
|
|
137
|
+
{ Year: "2021", Make: "Ford", Model: "Mondeo" },
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("extractAgGridData supports onlyColumns and valuesArray", async () => {
|
|
142
|
+
const { gridRoot } = createGridFixture();
|
|
143
|
+
gridRoot.getAnimations = () => [];
|
|
144
|
+
|
|
145
|
+
const subset = await extractAgGridData(gridRoot, { onlyColumns: ["Make"] });
|
|
146
|
+
const arrays = await extractAgGridData(gridRoot, { valuesArray: true });
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(subset, [{ Make: "Toyota" }, { Make: "Ford" }]);
|
|
149
|
+
assert.deepEqual(arrays.headers, ["Year", "Make", "Model"]);
|
|
150
|
+
assert.deepEqual(arrays.rows, [
|
|
151
|
+
["2020", "Toyota", "Celica"],
|
|
152
|
+
["2021", "Ford", "Mondeo"],
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("extractAgGridElements returns cell elements instead of text", async () => {
|
|
157
|
+
const { gridRoot, pinnedLeftRow0, centerRow0 } = createGridFixture();
|
|
158
|
+
gridRoot.getAnimations = () => [];
|
|
159
|
+
|
|
160
|
+
const rows = await extractAgGridElements(gridRoot);
|
|
161
|
+
|
|
162
|
+
assert.equal(rows[0].Year, pinnedLeftRow0.querySelectorAll(".ag-cell")[0]);
|
|
163
|
+
assert.equal(rows[0].Make, centerRow0.querySelectorAll(".ag-cell")[0]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("waitForAgGridAnimation waits for AG Grid animations and ignores aborts", async () => {
|
|
167
|
+
let resolved = false;
|
|
168
|
+
|
|
169
|
+
const target = new FakeElement({ classList: ["ag-root"] });
|
|
170
|
+
const root = new FakeElement();
|
|
171
|
+
root.getAnimations = () => [
|
|
172
|
+
{
|
|
173
|
+
effect: {
|
|
174
|
+
target,
|
|
175
|
+
getTiming: () => ({ iterations: 1 }),
|
|
176
|
+
},
|
|
177
|
+
finished: new Promise((resolve) => {
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
resolved = true;
|
|
180
|
+
resolve();
|
|
181
|
+
}, 10);
|
|
182
|
+
}),
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
effect: {
|
|
186
|
+
target,
|
|
187
|
+
getTiming: () => ({ iterations: 1 }),
|
|
188
|
+
},
|
|
189
|
+
finished: Promise.reject(Object.assign(new Error("aborted"), { name: "AbortError" })),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
effect: {
|
|
193
|
+
target: new FakeElement({ classList: ["spinner"] }),
|
|
194
|
+
getTiming: () => ({ iterations: 1 }),
|
|
195
|
+
},
|
|
196
|
+
finished: Promise.resolve(),
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
await waitForAgGridAnimation(root, { timeoutMs: 50 });
|
|
201
|
+
|
|
202
|
+
assert.equal(resolved, true);
|
|
203
|
+
});
|