@rickcedwhat/playwright-smart-table 3.0.0 → 3.2.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/README.md +84 -37
- package/dist/strategies/columns.d.ts +30 -0
- package/dist/strategies/columns.js +53 -0
- package/dist/strategies/fill.d.ts +7 -0
- package/dist/strategies/fill.js +88 -0
- package/dist/strategies/headers.d.ts +29 -0
- package/dist/strategies/headers.js +142 -0
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +92 -34
- package/dist/types.d.ts +104 -33
- package/dist/useTable.d.ts +6 -8
- package/dist/useTable.js +115 -120
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ const table = useTable(page.locator('#example'), {
|
|
|
81
81
|
headerSelector: 'thead th',
|
|
82
82
|
cellSelector: 'td',
|
|
83
83
|
// Strategy: Tell it how to find the next page
|
|
84
|
-
pagination:
|
|
84
|
+
pagination: PaginationStrategies.clickNext(() =>
|
|
85
85
|
page.getByRole('link', { name: 'Next' })
|
|
86
86
|
),
|
|
87
87
|
maxPages: 5 // Allow scanning up to 5 pages
|
|
@@ -91,8 +91,8 @@ await table.init();
|
|
|
91
91
|
// ✅ Verify Colleen is NOT visible initially
|
|
92
92
|
await expect(page.getByText("Colleen Hurst")).not.toBeVisible();
|
|
93
93
|
|
|
94
|
-
// Use
|
|
95
|
-
await expect(await table.
|
|
94
|
+
// Use searchForRow for pagination
|
|
95
|
+
await expect(await table.searchForRow({ Name: "Colleen Hurst" })).toBeVisible();
|
|
96
96
|
// NOTE: We're now on the page where Colleen Hurst exists (typically Page 2)
|
|
97
97
|
```
|
|
98
98
|
<!-- /embed: pagination -->
|
|
@@ -126,11 +126,11 @@ If your tests navigate deep into a paginated table, use `.reset()` to return to
|
|
|
126
126
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
127
127
|
// Navigate deep into the table by searching for a row on a later page
|
|
128
128
|
try {
|
|
129
|
-
await table.
|
|
130
|
-
} catch (e) {}
|
|
129
|
+
await table.searchForRow({ Name: 'Angelica Ramos' });
|
|
130
|
+
} catch (e) { }
|
|
131
131
|
|
|
132
132
|
// Reset internal state (and potentially UI) to initial page
|
|
133
|
-
await table.reset();
|
|
133
|
+
await table.reset();
|
|
134
134
|
await table.init(); // Re-init after reset
|
|
135
135
|
|
|
136
136
|
// Now subsequent searches start from the beginning
|
|
@@ -147,7 +147,7 @@ Efficiently extract all values from a specific column:
|
|
|
147
147
|
```typescript
|
|
148
148
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
149
149
|
// Quickly grab all text values from the "Office" column
|
|
150
|
-
const offices = await table.getColumnValues('Office');
|
|
150
|
+
const offices = await table.getColumnValues('Office');
|
|
151
151
|
expect(offices).toContain('Tokyo');
|
|
152
152
|
expect(offices.length).toBeGreaterThan(0);
|
|
153
153
|
```
|
|
@@ -193,8 +193,6 @@ For edge cases where auto-detection doesn't work (e.g., custom components, multi
|
|
|
193
193
|
|
|
194
194
|
<!-- embed: fill-custom-mappers -->
|
|
195
195
|
```typescript
|
|
196
|
-
const row = table.getByRow({ ID: '1' });
|
|
197
|
-
|
|
198
196
|
// Use custom input mappers for specific columns
|
|
199
197
|
await row.smartFill({
|
|
200
198
|
Name: 'John Updated',
|
|
@@ -231,7 +229,7 @@ const table = useTable(page.locator('.MuiDataGrid-root').first(), {
|
|
|
231
229
|
rowSelector: '.MuiDataGrid-row',
|
|
232
230
|
headerSelector: '.MuiDataGrid-columnHeader',
|
|
233
231
|
cellSelector: '.MuiDataGrid-cell',
|
|
234
|
-
pagination:
|
|
232
|
+
pagination: PaginationStrategies.clickNext(
|
|
235
233
|
(root) => root.getByRole("button", { name: "Go to next page" })
|
|
236
234
|
),
|
|
237
235
|
maxPages: 5,
|
|
@@ -256,7 +254,7 @@ const currentPageRow = table.getByRow({ "Last name": "Melisandre" });
|
|
|
256
254
|
await expect(currentPageRow).not.toBeVisible();
|
|
257
255
|
|
|
258
256
|
// Then find it across pages
|
|
259
|
-
const row = await table.
|
|
257
|
+
const row = await table.searchForRow({ "Last name": "Melisandre" });
|
|
260
258
|
const actionsCell = row.getCell('Actions');
|
|
261
259
|
await actionsCell.getByLabel("Select row").click();
|
|
262
260
|
```
|
|
@@ -328,8 +326,9 @@ await expect(table.getByRow({ Name: "Ghost User" })).not.toBeVisible();
|
|
|
328
326
|
Get row data as JSON:
|
|
329
327
|
<!-- embed: get-by-row-json -->
|
|
330
328
|
```typescript
|
|
331
|
-
// Get row data
|
|
332
|
-
const
|
|
329
|
+
// Get row data as JSON object
|
|
330
|
+
const row = table.getByRow({ Name: 'Airi Satou' });
|
|
331
|
+
const data = await row.toJSON();
|
|
333
332
|
// Returns: { Name: "Airi Satou", Position: "Accountant", Office: "Tokyo", ... }
|
|
334
333
|
|
|
335
334
|
expect(data).toHaveProperty('Name', 'Airi Satou');
|
|
@@ -337,15 +336,17 @@ expect(data).toHaveProperty('Position');
|
|
|
337
336
|
```
|
|
338
337
|
<!-- /embed: get-by-row-json -->
|
|
339
338
|
|
|
340
|
-
#### <a name="
|
|
339
|
+
#### <a name="getallcurrentrows"></a>`getAllCurrentRows(options?)`
|
|
341
340
|
|
|
342
|
-
**Purpose:** Inclusive retrieval - gets all rows matching optional filters.
|
|
341
|
+
**Purpose:** Inclusive retrieval - gets all rows on the current page matching optional filters.
|
|
343
342
|
|
|
344
|
-
**Best for:** Checking existence, validating sort order, bulk data extraction.
|
|
343
|
+
**Best for:** Checking existence, validating sort order, bulk data extraction on the current page.
|
|
344
|
+
|
|
345
|
+
> **Note:** `getAllRows` is deprecated and will be removed in a future major version. Use `getAllCurrentRows` instead. The deprecated method still works for backwards compatibility.
|
|
345
346
|
|
|
346
347
|
**Type Signature:**
|
|
347
348
|
```typescript
|
|
348
|
-
|
|
349
|
+
getAllCurrentRows: <T extends { asJSON?: boolean }>(
|
|
349
350
|
options?: { filter?: Record<string, any>, exact?: boolean } & T
|
|
350
351
|
) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
351
352
|
```
|
|
@@ -354,17 +355,17 @@ getAllRows: <T extends { asJSON?: boolean }>(
|
|
|
354
355
|
```typescript
|
|
355
356
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
356
357
|
// 1. Get ALL rows on the current page
|
|
357
|
-
const allRows = await table.
|
|
358
|
+
const allRows = await table.getAllCurrentRows();
|
|
358
359
|
expect(allRows.length).toBeGreaterThan(0);
|
|
359
360
|
|
|
360
361
|
// 2. Get subset of rows (Filtering)
|
|
361
|
-
const tokyoUsers = await table.
|
|
362
|
+
const tokyoUsers = await table.getAllCurrentRows({
|
|
362
363
|
filter: { Office: 'Tokyo' }
|
|
363
364
|
});
|
|
364
365
|
expect(tokyoUsers.length).toBeGreaterThan(0);
|
|
365
366
|
|
|
366
367
|
// 3. Dump data to JSON
|
|
367
|
-
const data = await table.
|
|
368
|
+
const data = await table.getAllCurrentRows({ asJSON: true });
|
|
368
369
|
console.log(data); // [{ Name: "Airi Satou", ... }, ...]
|
|
369
370
|
expect(data.length).toBeGreaterThan(0);
|
|
370
371
|
expect(data[0]).toHaveProperty('Name');
|
|
@@ -375,7 +376,7 @@ Filter rows with exact match:
|
|
|
375
376
|
<!-- embed: get-all-rows-exact -->
|
|
376
377
|
```typescript
|
|
377
378
|
// Get rows with exact match (default is fuzzy/contains match)
|
|
378
|
-
const exactMatches = await table.
|
|
379
|
+
const exactMatches = await table.getAllCurrentRows({
|
|
379
380
|
filter: { Office: 'Tokyo' },
|
|
380
381
|
exact: true // Requires exact string match
|
|
381
382
|
});
|
|
@@ -404,7 +405,7 @@ Basic usage:
|
|
|
404
405
|
```typescript
|
|
405
406
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
406
407
|
// Quickly grab all text values from the "Office" column
|
|
407
|
-
const offices = await table.getColumnValues('Office');
|
|
408
|
+
const offices = await table.getColumnValues('Office');
|
|
408
409
|
expect(offices).toContain('Tokyo');
|
|
409
410
|
expect(offices.length).toBeGreaterThan(0);
|
|
410
411
|
```
|
|
@@ -559,11 +560,17 @@ A `SmartRow` extends Playwright's `Locator` with table-aware methods.
|
|
|
559
560
|
<!-- embed-type: SmartRow -->
|
|
560
561
|
```typescript
|
|
561
562
|
export type SmartRow = Locator & {
|
|
563
|
+
getRequestIndex(): number | undefined; // Helper to get the row index if known
|
|
564
|
+
rowIndex?: number;
|
|
562
565
|
getCell(column: string): Locator;
|
|
563
566
|
toJSON(): Promise<Record<string, string>>;
|
|
564
567
|
/**
|
|
565
568
|
* Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
|
|
566
569
|
*/
|
|
570
|
+
fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
571
|
+
/**
|
|
572
|
+
* Alias for fill() to avoid conflict with Locator.fill()
|
|
573
|
+
*/
|
|
567
574
|
smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
568
575
|
};
|
|
569
576
|
```
|
|
@@ -582,30 +589,70 @@ Configuration options for `useTable()`.
|
|
|
582
589
|
|
|
583
590
|
<!-- embed-type: TableConfig -->
|
|
584
591
|
```typescript
|
|
592
|
+
/**
|
|
593
|
+
* Output Strategy:
|
|
594
|
+
* - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).
|
|
595
|
+
* - 'console': Standard console logs (Default).
|
|
596
|
+
*/
|
|
597
|
+
output?: 'console' | 'error';
|
|
598
|
+
includeTypes?: boolean;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export type FillStrategy = (options: {
|
|
602
|
+
row: SmartRow;
|
|
603
|
+
columnName: string;
|
|
604
|
+
value: any;
|
|
605
|
+
index: number;
|
|
606
|
+
page: Page;
|
|
607
|
+
rootLocator: Locator;
|
|
608
|
+
table: TableResult; // The parent table instance
|
|
609
|
+
fillOptions?: FillOptions;
|
|
610
|
+
}) => Promise<void>;
|
|
611
|
+
|
|
612
|
+
export type { HeaderStrategy } from './strategies/headers';
|
|
613
|
+
export type { ColumnStrategy } from './strategies/columns';
|
|
614
|
+
import { HeaderStrategy } from './strategies/headers';
|
|
615
|
+
import { ColumnStrategy } from './strategies/columns';
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Configuration options for useTable.
|
|
619
|
+
*/
|
|
585
620
|
export interface TableConfig {
|
|
586
|
-
|
|
587
|
-
headerSelector?:
|
|
588
|
-
|
|
621
|
+
/** Selector for the table headers */
|
|
622
|
+
headerSelector?: string;
|
|
623
|
+
/** Selector for the table rows */
|
|
624
|
+
rowSelector?: string;
|
|
625
|
+
/** Selector for the cells within a row */
|
|
626
|
+
cellSelector?: string;
|
|
627
|
+
/** Strategy for filling forms within the table */
|
|
628
|
+
fillStrategy?: FillStrategy;
|
|
629
|
+
/** Strategy for discovering headers */
|
|
630
|
+
headerStrategy?: HeaderStrategy;
|
|
631
|
+
/** Strategy for navigating to columns */
|
|
632
|
+
columnStrategy?: ColumnStrategy;
|
|
633
|
+
/** Number of pages to scan for verification */
|
|
634
|
+
maxPages?: number;
|
|
635
|
+
|
|
636
|
+
/** Pagination Strategy */
|
|
589
637
|
pagination?: PaginationStrategy;
|
|
638
|
+
/** Sorting Strategy */
|
|
590
639
|
sorting?: SortingStrategy;
|
|
591
|
-
|
|
592
|
-
/**
|
|
640
|
+
/**
|
|
593
641
|
* Hook to rename columns dynamically.
|
|
594
|
-
* * @param args.text - The default innerText of the header.
|
|
595
|
-
* @param args.index - The column index.
|
|
596
|
-
* @param args.locator - The specific header cell locator.
|
|
597
642
|
*/
|
|
598
643
|
headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
|
|
644
|
+
/** Automatically scroll to table on init */
|
|
599
645
|
autoScroll?: boolean;
|
|
600
|
-
/**
|
|
601
|
-
* Enable debug mode to log internal state to console.
|
|
602
|
-
*/
|
|
646
|
+
/** Enable debug logs */
|
|
603
647
|
debug?: boolean;
|
|
604
|
-
/**
|
|
605
|
-
* Strategy to reset the table to the initial page.
|
|
606
|
-
* Called when table.reset() is invoked.
|
|
607
|
-
*/
|
|
648
|
+
/** Reset hook */
|
|
608
649
|
onReset?: (context: TableContext) => Promise<void>;
|
|
650
|
+
/**
|
|
651
|
+
* Custom resolver for finding a cell.
|
|
652
|
+
* Overrides cellSelector logic if provided.
|
|
653
|
+
* Useful for virtualized tables where nth() index doesn't match DOM index.
|
|
654
|
+
*/
|
|
655
|
+
cellResolver?: (args: { row: Locator, columnName: string, columnIndex: number, rowIndex?: number }) => Locator;
|
|
609
656
|
}
|
|
610
657
|
```
|
|
611
658
|
<!-- /embed-type: TableConfig -->
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { StrategyContext } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Defines the contract for a column navigation strategy.
|
|
4
|
+
* It is responsible for ensuring a specific column is visible/focused,
|
|
5
|
+
* typically by scrolling or navigating to it.
|
|
6
|
+
*/
|
|
7
|
+
export type ColumnStrategy = (context: StrategyContext & {
|
|
8
|
+
column: string;
|
|
9
|
+
index: number;
|
|
10
|
+
rowIndex?: number;
|
|
11
|
+
}) => Promise<void>;
|
|
12
|
+
export declare const ColumnStrategies: {
|
|
13
|
+
/**
|
|
14
|
+
* Default strategy: Assumes column is accessible or standard scrolling works.
|
|
15
|
+
* No specific action taken other than what Playwright's default locator handling does.
|
|
16
|
+
*/
|
|
17
|
+
default: () => Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Strategy that clicks into the table to establish focus and then uses the Right Arrow key
|
|
20
|
+
* to navigate to the target column index.
|
|
21
|
+
*
|
|
22
|
+
* Useful for canvas-based grids like Glide where DOM scrolling might not be enough for interaction
|
|
23
|
+
* or where keyboard navigation is the primary way to move focus.
|
|
24
|
+
*/
|
|
25
|
+
keyboard: (context: StrategyContext & {
|
|
26
|
+
column: string;
|
|
27
|
+
index: number;
|
|
28
|
+
rowIndex?: number;
|
|
29
|
+
}) => Promise<void>;
|
|
30
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ColumnStrategies = void 0;
|
|
13
|
+
exports.ColumnStrategies = {
|
|
14
|
+
/**
|
|
15
|
+
* Default strategy: Assumes column is accessible or standard scrolling works.
|
|
16
|
+
* No specific action taken other than what Playwright's default locator handling does.
|
|
17
|
+
*/
|
|
18
|
+
default: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
19
|
+
// No-op
|
|
20
|
+
}),
|
|
21
|
+
/**
|
|
22
|
+
* Strategy that clicks into the table to establish focus and then uses the Right Arrow key
|
|
23
|
+
* to navigate to the target column index.
|
|
24
|
+
*
|
|
25
|
+
* Useful for canvas-based grids like Glide where DOM scrolling might not be enough for interaction
|
|
26
|
+
* or where keyboard navigation is the primary way to move focus.
|
|
27
|
+
*/
|
|
28
|
+
keyboard: (context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
29
|
+
const { root, page, index, rowLocator, rowIndex } = context;
|
|
30
|
+
if (typeof rowIndex !== 'number') {
|
|
31
|
+
throw new Error('Row index is required for keyboard navigation');
|
|
32
|
+
}
|
|
33
|
+
console.log(`[ColumnStrat:keyboard] Using Row Index Navigation: Row ${rowIndex}, Col ${index}`);
|
|
34
|
+
yield root.focus();
|
|
35
|
+
yield page.waitForTimeout(200);
|
|
36
|
+
// Robust Navigation:
|
|
37
|
+
// 1. Jump to Top-Left (Reset) - Sequence for Cross-OS (Mac/Windows)
|
|
38
|
+
yield page.keyboard.press('Control+Home');
|
|
39
|
+
yield page.keyboard.press('Meta+ArrowUp'); // Mac Go-To-Top
|
|
40
|
+
yield page.keyboard.press('Home'); // Ensure start of row
|
|
41
|
+
yield page.waitForTimeout(300);
|
|
42
|
+
// 2. Move Down to Target Row
|
|
43
|
+
for (let i = 0; i < rowIndex; i++) {
|
|
44
|
+
yield page.keyboard.press('ArrowDown');
|
|
45
|
+
}
|
|
46
|
+
// 3. Move Right to Target Column
|
|
47
|
+
for (let i = 0; i < index; i++) {
|
|
48
|
+
yield page.keyboard.press('ArrowRight');
|
|
49
|
+
}
|
|
50
|
+
yield page.waitForTimeout(100);
|
|
51
|
+
yield page.waitForTimeout(100);
|
|
52
|
+
})
|
|
53
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FillStrategy } from '../types';
|
|
2
|
+
export declare const FillStrategies: {
|
|
3
|
+
/**
|
|
4
|
+
* Default strategy: Detects input type and fills accordingly (Text, Select, Checkbox, ContentEditable).
|
|
5
|
+
*/
|
|
6
|
+
default: ({ row, columnName, value, fillOptions }: Parameters<FillStrategy>[0]) => Promise<void>;
|
|
7
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.FillStrategies = void 0;
|
|
13
|
+
exports.FillStrategies = {
|
|
14
|
+
/**
|
|
15
|
+
* Default strategy: Detects input type and fills accordingly (Text, Select, Checkbox, ContentEditable).
|
|
16
|
+
*/
|
|
17
|
+
default: (_a) => __awaiter(void 0, [_a], void 0, function* ({ row, columnName, value, fillOptions }) {
|
|
18
|
+
var _b;
|
|
19
|
+
const cell = row.getCell(columnName);
|
|
20
|
+
// Use custom input mapper for this column if provided, otherwise auto-detect
|
|
21
|
+
let inputLocator;
|
|
22
|
+
if ((_b = fillOptions === null || fillOptions === void 0 ? void 0 : fillOptions.inputMappers) === null || _b === void 0 ? void 0 : _b[columnName]) {
|
|
23
|
+
inputLocator = fillOptions.inputMappers[columnName](cell);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Auto-detect input type
|
|
27
|
+
// Check for text input
|
|
28
|
+
const textInput = cell.locator('input[type="text"], input:not([type]), textarea').first();
|
|
29
|
+
const textInputCount = yield textInput.count().catch(() => 0);
|
|
30
|
+
// Check for select
|
|
31
|
+
const select = cell.locator('select').first();
|
|
32
|
+
const selectCount = yield select.count().catch(() => 0);
|
|
33
|
+
// Check for checkbox/radio
|
|
34
|
+
const checkbox = cell.locator('input[type="checkbox"], input[type="radio"], [role="checkbox"]').first();
|
|
35
|
+
const checkboxCount = yield checkbox.count().catch(() => 0);
|
|
36
|
+
// Check for contenteditable or div-based inputs
|
|
37
|
+
const contentEditable = cell.locator('[contenteditable="true"]').first();
|
|
38
|
+
const contentEditableCount = yield contentEditable.count().catch(() => 0);
|
|
39
|
+
// Determine which input to use (prioritize by commonality)
|
|
40
|
+
if (textInputCount > 0 && selectCount === 0 && checkboxCount === 0) {
|
|
41
|
+
inputLocator = textInput;
|
|
42
|
+
}
|
|
43
|
+
else if (selectCount > 0) {
|
|
44
|
+
inputLocator = select;
|
|
45
|
+
}
|
|
46
|
+
else if (checkboxCount > 0) {
|
|
47
|
+
inputLocator = checkbox;
|
|
48
|
+
}
|
|
49
|
+
else if (contentEditableCount > 0) {
|
|
50
|
+
inputLocator = contentEditable;
|
|
51
|
+
}
|
|
52
|
+
else if (textInputCount > 0) {
|
|
53
|
+
// Fallback to text input even if others exist
|
|
54
|
+
inputLocator = textInput;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// No input found - try to click the cell itself (might trigger an editor)
|
|
58
|
+
inputLocator = cell;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Fill based on value type and input type
|
|
62
|
+
const inputTag = yield inputLocator.evaluate((el) => el.tagName.toLowerCase()).catch(() => 'unknown');
|
|
63
|
+
const inputType = yield inputLocator.getAttribute('type').catch(() => null);
|
|
64
|
+
const isContentEditable = yield inputLocator.getAttribute('contenteditable').catch(() => null);
|
|
65
|
+
// console.log(`[SmartTable] Filling "${columnName}" with value "${value}" (input: ${inputTag}, type: ${inputType})`);
|
|
66
|
+
if (inputType === 'checkbox' || inputType === 'radio') {
|
|
67
|
+
// Boolean value for checkbox/radio
|
|
68
|
+
const shouldBeChecked = Boolean(value);
|
|
69
|
+
const isChecked = yield inputLocator.isChecked().catch(() => false);
|
|
70
|
+
if (isChecked !== shouldBeChecked) {
|
|
71
|
+
yield inputLocator.click();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (inputTag === 'select') {
|
|
75
|
+
// Select dropdown
|
|
76
|
+
yield inputLocator.selectOption(String(value));
|
|
77
|
+
}
|
|
78
|
+
else if (isContentEditable === 'true') {
|
|
79
|
+
// Contenteditable div
|
|
80
|
+
yield inputLocator.click();
|
|
81
|
+
yield inputLocator.fill(String(value));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Text input, textarea, or generic
|
|
85
|
+
yield inputLocator.fill(String(value));
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { StrategyContext } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Defines the contract for a header retrieval strategy.
|
|
4
|
+
* Returns a list of unique header names found in the table.
|
|
5
|
+
*/
|
|
6
|
+
export type HeaderStrategy = (context: StrategyContext) => Promise<string[]>;
|
|
7
|
+
export declare const HeaderStrategies: {
|
|
8
|
+
/**
|
|
9
|
+
* Default strategy: Returns only the headers currently visible in the DOM.
|
|
10
|
+
* This is fast but won't find virtualized columns off-screen.
|
|
11
|
+
*/
|
|
12
|
+
visible: ({ config, resolve, root }: StrategyContext) => Promise<string[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Scans for headers by finding a scrollable container and setting scrollLeft.
|
|
15
|
+
*/
|
|
16
|
+
scrollRight: (context: StrategyContext, options?: {
|
|
17
|
+
limit?: number;
|
|
18
|
+
selector?: string;
|
|
19
|
+
scrollAmount?: number;
|
|
20
|
+
}) => Promise<string[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Strategy that clicks into the table to establish focus and then uses the Right Arrow key
|
|
23
|
+
* to navigate cell-by-cell, collecting headers found along the way.
|
|
24
|
+
*/
|
|
25
|
+
keyboard: (context: StrategyContext, options?: {
|
|
26
|
+
limit?: number;
|
|
27
|
+
maxSilentClicks?: number;
|
|
28
|
+
}) => Promise<string[]>;
|
|
29
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.HeaderStrategies = void 0;
|
|
13
|
+
exports.HeaderStrategies = {
|
|
14
|
+
/**
|
|
15
|
+
* Default strategy: Returns only the headers currently visible in the DOM.
|
|
16
|
+
* This is fast but won't find virtualized columns off-screen.
|
|
17
|
+
*/
|
|
18
|
+
visible: (_a) => __awaiter(void 0, [_a], void 0, function* ({ config, resolve, root }) {
|
|
19
|
+
const headerLoc = resolve(config.headerSelector, root);
|
|
20
|
+
try {
|
|
21
|
+
// Wait for at least one header to be visible
|
|
22
|
+
yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
// Ignore hydration/timeout issues, return what we have
|
|
26
|
+
}
|
|
27
|
+
const texts = yield headerLoc.allInnerTexts();
|
|
28
|
+
return texts.map(t => t.trim());
|
|
29
|
+
}),
|
|
30
|
+
/**
|
|
31
|
+
* Scans for headers by finding a scrollable container and setting scrollLeft.
|
|
32
|
+
*/
|
|
33
|
+
scrollRight: (context, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
|
+
var _a, _b;
|
|
35
|
+
const { resolve, config, root, page } = context;
|
|
36
|
+
const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : 20;
|
|
37
|
+
const scrollAmount = (_b = options === null || options === void 0 ? void 0 : options.scrollAmount) !== null && _b !== void 0 ? _b : 300;
|
|
38
|
+
const collectedHeaders = new Set();
|
|
39
|
+
const getVisible = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
40
|
+
const headerLoc = resolve(config.headerSelector, root);
|
|
41
|
+
const texts = yield headerLoc.allInnerTexts();
|
|
42
|
+
return texts.map(t => t.trim());
|
|
43
|
+
});
|
|
44
|
+
// Initial capture
|
|
45
|
+
let currentHeaders = yield getVisible();
|
|
46
|
+
currentHeaders.forEach(h => collectedHeaders.add(h));
|
|
47
|
+
// Find scroller using JS for better iframe/shadow support
|
|
48
|
+
const scrollerHandle = yield root.evaluateHandle((el, selector) => {
|
|
49
|
+
if (selector && el.matches(selector))
|
|
50
|
+
return el;
|
|
51
|
+
const effectiveSelector = selector || '.dvn-scroller';
|
|
52
|
+
const ancestor = el.closest(effectiveSelector);
|
|
53
|
+
if (ancestor)
|
|
54
|
+
return ancestor;
|
|
55
|
+
return document.querySelector(effectiveSelector);
|
|
56
|
+
}, options === null || options === void 0 ? void 0 : options.selector);
|
|
57
|
+
const isScrollerFound = yield scrollerHandle.evaluate(el => !!el);
|
|
58
|
+
if (isScrollerFound) {
|
|
59
|
+
yield scrollerHandle.evaluate(el => el.scrollLeft = 0);
|
|
60
|
+
yield page.waitForTimeout(200);
|
|
61
|
+
for (let i = 0; i < limit; i++) {
|
|
62
|
+
const sizeBefore = collectedHeaders.size;
|
|
63
|
+
yield scrollerHandle.evaluate((el, amount) => el.scrollLeft += amount, scrollAmount);
|
|
64
|
+
yield page.waitForTimeout(300);
|
|
65
|
+
const newHeaders = yield getVisible();
|
|
66
|
+
console.log(`[HeaderStrat:scrollRight] Scrolled ${scrollAmount}, found: ${newHeaders.length} visible.`);
|
|
67
|
+
newHeaders.forEach(h => collectedHeaders.add(h));
|
|
68
|
+
if (collectedHeaders.size === sizeBefore) {
|
|
69
|
+
yield scrollerHandle.evaluate((el, amount) => el.scrollLeft += amount, scrollAmount);
|
|
70
|
+
yield page.waitForTimeout(300);
|
|
71
|
+
const retryHeaders = yield getVisible();
|
|
72
|
+
retryHeaders.forEach(h => collectedHeaders.add(h));
|
|
73
|
+
if (collectedHeaders.size === sizeBefore)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
console.warn("HeaderStrategies.scrollRight: Could not find scroller. Returning visible headers.");
|
|
80
|
+
}
|
|
81
|
+
// Scroll back to start
|
|
82
|
+
yield scrollerHandle.evaluate(el => el.scrollLeft = 0);
|
|
83
|
+
yield page.waitForTimeout(200);
|
|
84
|
+
return Array.from(collectedHeaders);
|
|
85
|
+
}),
|
|
86
|
+
/**
|
|
87
|
+
* Strategy that clicks into the table to establish focus and then uses the Right Arrow key
|
|
88
|
+
* to navigate cell-by-cell, collecting headers found along the way.
|
|
89
|
+
*/
|
|
90
|
+
keyboard: (context, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
var _a, _b;
|
|
92
|
+
const { resolve, config, root, page } = context;
|
|
93
|
+
const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : 100;
|
|
94
|
+
const maxSilentClicks = (_b = options === null || options === void 0 ? void 0 : options.maxSilentClicks) !== null && _b !== void 0 ? _b : 10;
|
|
95
|
+
const collectedHeaders = new Set();
|
|
96
|
+
const getVisible = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
97
|
+
const headerLoc = resolve(config.headerSelector, root);
|
|
98
|
+
const texts = yield headerLoc.allInnerTexts();
|
|
99
|
+
return texts.map(t => t.trim());
|
|
100
|
+
});
|
|
101
|
+
// 1. Initial capture
|
|
102
|
+
let currentHeaders = yield getVisible();
|
|
103
|
+
currentHeaders.forEach(h => collectedHeaders.add(h));
|
|
104
|
+
// 2. Click to focus
|
|
105
|
+
// Try to find the canvas sibling specifically for Glide, otherwise click root
|
|
106
|
+
const canvas = root.locator('xpath=ancestor::div').locator('canvas').first();
|
|
107
|
+
if ((yield canvas.count()) > 0) {
|
|
108
|
+
// Click lower in the canvas to hit a data cell instead of header
|
|
109
|
+
// Adjusted to y=60 to target Row 1
|
|
110
|
+
yield canvas.click({ position: { x: 50, y: 60 }, force: true }).catch(() => { });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
yield root.click({ position: { x: 10, y: 10 }, force: true }).catch(() => { });
|
|
114
|
+
}
|
|
115
|
+
// Reset to home
|
|
116
|
+
yield page.keyboard.press('Control+Home');
|
|
117
|
+
yield page.keyboard.press('Home');
|
|
118
|
+
yield page.waitForTimeout(200);
|
|
119
|
+
currentHeaders = yield getVisible();
|
|
120
|
+
currentHeaders.forEach(h => collectedHeaders.add(h));
|
|
121
|
+
// 3. Navigate right loop
|
|
122
|
+
let silentCounter = 0;
|
|
123
|
+
for (let i = 0; i < limit; i++) {
|
|
124
|
+
const sizeBefore = collectedHeaders.size;
|
|
125
|
+
yield page.keyboard.press('ArrowRight');
|
|
126
|
+
yield page.waitForTimeout(100);
|
|
127
|
+
const newHeaders = yield getVisible();
|
|
128
|
+
console.log(`[HeaderStrat:keyboard] Step ${i}, found visible: ${newHeaders}`);
|
|
129
|
+
newHeaders.forEach(h => collectedHeaders.add(h));
|
|
130
|
+
if (collectedHeaders.size === sizeBefore) {
|
|
131
|
+
silentCounter++;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
silentCounter = 0;
|
|
135
|
+
}
|
|
136
|
+
if (silentCounter >= maxSilentClicks) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return Array.from(collectedHeaders);
|
|
141
|
+
}),
|
|
142
|
+
};
|
package/dist/typeContext.d.ts
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* This file is generated by scripts/embed-types.js
|
|
4
4
|
* It contains the raw text of types.ts to provide context for LLM prompts.
|
|
5
5
|
*/
|
|
6
|
-
export declare const TYPE_CONTEXT = "\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\nexport type SmartRow = Locator & {\n getCell(column: string): Locator;\n toJSON(): Promise<Record<string, string>>;\n /**\n * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).\n */\n smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;\n};\n\nexport type StrategyContext = TableContext;\n\n/**\n * Defines the contract for a sorting strategy.\n */\nexport interface SortingStrategy {\n /**\n * Performs the sort action on a column.\n */\n doSort(options: {\n columnName: string;\n direction: 'asc' | 'desc';\n context: StrategyContext;\n }): Promise<void>;\n\n /**\n * Retrieves the current sort state of a column.\n */\n getSortState(options: {\n columnName: string;\n context: StrategyContext;\n }): Promise<'asc' | 'desc' | 'none'>;\n}\n\nexport interface TableContext {\n root: Locator;\n config: FinalTableConfig;\n page: Page;\n resolve: (selector: Selector, parent: Locator | Page) => Locator;\n}\n\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;\n\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n includeTypes?: boolean;\n}\n\nexport interface TableConfig {\n
|
|
6
|
+
export declare const TYPE_CONTEXT = "\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\nexport type SmartRow = Locator & {\n getRequestIndex(): number | undefined; // Helper to get the row index if known\n rowIndex?: number;\n getCell(column: string): Locator;\n toJSON(): Promise<Record<string, string>>;\n /**\n * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).\n */\n fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;\n /**\n * Alias for fill() to avoid conflict with Locator.fill() \n */\n smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;\n};\n\nexport type StrategyContext = TableContext & { rowLocator?: Locator; rowIndex?: number };\n\n/**\n * Defines the contract for a sorting strategy.\n */\nexport interface SortingStrategy {\n /**\n * Performs the sort action on a column.\n */\n doSort(options: {\n columnName: string;\n direction: 'asc' | 'desc';\n context: StrategyContext;\n }): Promise<void>;\n\n /**\n * Retrieves the current sort state of a column.\n */\n getSortState(options: {\n columnName: string;\n context: StrategyContext;\n }): Promise<'asc' | 'desc' | 'none'>;\n}\n\nexport interface TableContext {\n root: Locator;\n config: FinalTableConfig;\n page: Page;\n resolve: (selector: Selector, parent: Locator | Page) => Locator;\n}\n\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport type DedupeStrategy = (row: SmartRow) => string | number | Promise<string | number>;\n\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n includeTypes?: boolean;\n}\n\nexport type FillStrategy = (options: {\n row: SmartRow;\n columnName: string;\n value: any;\n index: number;\n page: Page;\n rootLocator: Locator;\n table: TableResult; // The parent table instance\n fillOptions?: FillOptions;\n}) => Promise<void>;\n\nexport type { HeaderStrategy } from './strategies/headers';\nexport type { ColumnStrategy } from './strategies/columns';\n\n/**\n * Configuration options for useTable.\n */\nexport interface TableConfig {\n /** Selector for the table headers */\n headerSelector?: string;\n /** Selector for the table rows */\n rowSelector?: string;\n /** Selector for the cells within a row */\n cellSelector?: string;\n /** Strategy for filling forms within the table */\n fillStrategy?: FillStrategy;\n /** Strategy for discovering headers */\n headerStrategy?: HeaderStrategy;\n /** Strategy for navigating to columns */\n columnStrategy?: ColumnStrategy;\n /** Number of pages to scan for verification */\n maxPages?: number;\n\n /** Pagination Strategy */\n pagination?: PaginationStrategy;\n /** Sorting Strategy */\n sorting?: SortingStrategy;\n /** \n * Hook to rename columns dynamically.\n */\n headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n /** Automatically scroll to table on init */\n autoScroll?: boolean;\n /** Enable debug logs */\n debug?: boolean;\n /** Reset hook */\n onReset?: (context: TableContext) => Promise<void>;\n /**\n * Custom resolver for finding a cell. \n * Overrides cellSelector logic if provided.\n * Useful for virtualized tables where nth() index doesn't match DOM index.\n */\n cellResolver?: (args: { row: Locator, columnName: string, columnIndex: number, rowIndex?: number }) => Locator;\n}\n\nexport interface FinalTableConfig extends TableConfig {\n headerSelector: string;\n rowSelector: string;\n cellSelector: string;\n fillStrategy: FillStrategy;\n headerStrategy: HeaderStrategy;\n columnStrategy: ColumnStrategy;\n maxPages: number;\n pagination: PaginationStrategy;\n autoScroll: boolean;\n debug: boolean;\n headerTransformer: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n onReset: (context: TableContext) => Promise<void>;\n cellResolver?: (args: { row: Locator, columnName: string, columnIndex: number, rowIndex?: number }) => Locator;\n}\n\nexport interface FillOptions {\n /**\n * Custom input mappers for specific columns.\n * Maps column names to functions that return the input locator for that cell.\n */\n inputMappers?: Record<string, (cell: Locator) => Locator>;\n}\n\nexport interface TableResult {\n /**\n * Initializes the table by resolving headers. Must be called before using sync methods.\n * @param options Optional timeout for header resolution (default: 3000ms)\n */\n init(options?: { timeout?: number }): Promise<TableResult>;\n\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n /**\n * Finds a row on the current page only. Returns immediately (sync).\n * Throws error if table is not initialized.\n */\n getByRow: {\n (index: number): SmartRow;\n (\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean }\n ): SmartRow;\n };\n\n /**\n * Searches for a row across all available data using the configured strategy (pagination, scroll, etc.).\n * Auto-initializes if needed.\n */\n searchForRow: (\n filters: Record<string, string | RegExp | number>,\n options?: { exact?: boolean, maxPages?: number }\n ) => Promise<SmartRow>;\n\n /**\n * Manually scrolls to a column using the configured ColumnStrategy.\n */\n scrollToColumn: (columnName: string) => Promise<void>;\n\n getAllCurrentRows: <T extends { asJSON?: boolean }>(\n options?: { filter?: Record<string, any>, exact?: boolean } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n /**\n * @deprecated Use getAllCurrentRows instead. This method will be removed in a future major version.\n */\n getAllRows: <T extends { asJSON?: boolean }>(\n options?: { filter?: Record<string, any>, exact?: boolean } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n generateConfigPrompt: (options?: PromptOptions) => Promise<void>;\n generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;\n\n /**\n * Resets the table state (clears cache, flags) and invokes the onReset strategy.\n */\n reset: () => Promise<void>;\n\n /**\n * Scans a specific column across all pages and returns the values.\n */\n getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;\n\n /**\n * Provides access to sorting actions and assertions.\n */\n sorting: {\n /**\n * Applies the configured sorting strategy to the specified column.\n * @param columnName The name of the column to sort.\n * @param direction The direction to sort ('asc' or 'desc').\n */\n apply(columnName: string, direction: 'asc' | 'desc'): Promise<void>;\n /**\n * Gets the current sort state of a column using the configured sorting strategy.\n * @param columnName The name of the column to check.\n * @returns A promise that resolves to 'asc', 'desc', or 'none'.\n */\n getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;\n };\n\n /**\n * Iterates through paginated table data, calling the callback for each iteration.\n * Callback return values are automatically appended to allData, which is returned.\n */\n iterateThroughTable: <T = any>(\n callback: (context: {\n index: number;\n isFirst: boolean;\n isLast: boolean;\n rows: SmartRow[];\n allData: T[];\n table: RestrictedTableResult;\n }) => T | Promise<T>,\n options?: {\n pagination?: PaginationStrategy;\n dedupeStrategy?: DedupeStrategy;\n maxIterations?: number;\n getIsFirst?: (context: { index: number }) => boolean;\n getIsLast?: (context: { index: number, paginationResult: boolean }) => boolean;\n onFirst?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;\n onLast?: (context: { index: number, rows: SmartRow[], allData: any[] }) => void | Promise<void>;\n }\n ) => Promise<T[]>;\n}\n\n/**\n * Restricted table result that excludes methods that shouldn't be called during iteration.\n */\nexport type RestrictedTableResult = Omit<TableResult, 'searchForRow' | 'iterateThroughTable' | 'reset' | 'getAllRows'>;\n";
|