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/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,4 @@
1
+ {
2
+ "status": "passed",
3
+ "failedTests": []
4
+ }
@@ -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
+ });
@@ -0,0 +1,6 @@
1
+ import { runAgGridDataSuite } from "./shared/run-ag-grid-data-suite.js";
2
+
3
+ runAgGridDataSuite({
4
+ pagePath: "/v33/index.html",
5
+ versionLabel: "v33",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { runAgGridDataSuite } from "./shared/run-ag-grid-data-suite.js";
2
+
3
+ runAgGridDataSuite({
4
+ pagePath: "/v34/index.html",
5
+ versionLabel: "v34",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { runAgGridDataSuite } from "./shared/run-ag-grid-data-suite.js";
2
+
3
+ runAgGridDataSuite({
4
+ pagePath: "/index.html",
5
+ versionLabel: "v35",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { runAgGridElementsSuite } from "./shared/run-ag-grid-elements-suite.js";
2
+
3
+ runAgGridElementsSuite({
4
+ pagePath: "/v33/index.html",
5
+ versionLabel: "v33",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { runAgGridElementsSuite } from "./shared/run-ag-grid-elements-suite.js";
2
+
3
+ runAgGridElementsSuite({
4
+ pagePath: "/v34/index.html",
5
+ versionLabel: "v34",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { runAgGridElementsSuite } from "./shared/run-ag-grid-elements-suite.js";
2
+
3
+ runAgGridElementsSuite({
4
+ pagePath: "/index.html",
5
+ versionLabel: "v35",
6
+ });
@@ -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
+ }