@rickcedwhat/playwright-smart-table 3.1.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 +77 -31
- 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 +87 -30
- package/dist/types.d.ts +101 -27
- package/dist/useTable.d.ts +6 -8
- package/dist/useTable.js +114 -111
- 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
|
|
@@ -127,10 +127,10 @@ If your tests navigate deep into a paginated table, use `.reset()` to return to
|
|
|
127
127
|
// Navigate deep into the table by searching for a row on a later page
|
|
128
128
|
try {
|
|
129
129
|
await table.searchForRow({ Name: 'Angelica Ramos' });
|
|
130
|
-
} catch (e) {}
|
|
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,
|
|
@@ -338,15 +336,17 @@ expect(data).toHaveProperty('Position');
|
|
|
338
336
|
```
|
|
339
337
|
<!-- /embed: get-by-row-json -->
|
|
340
338
|
|
|
341
|
-
#### <a name="
|
|
339
|
+
#### <a name="getallcurrentrows"></a>`getAllCurrentRows(options?)`
|
|
340
|
+
|
|
341
|
+
**Purpose:** Inclusive retrieval - gets all rows on the current page matching optional filters.
|
|
342
342
|
|
|
343
|
-
**
|
|
343
|
+
**Best for:** Checking existence, validating sort order, bulk data extraction on the current page.
|
|
344
344
|
|
|
345
|
-
**
|
|
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.
|
|
346
346
|
|
|
347
347
|
**Type Signature:**
|
|
348
348
|
```typescript
|
|
349
|
-
|
|
349
|
+
getAllCurrentRows: <T extends { asJSON?: boolean }>(
|
|
350
350
|
options?: { filter?: Record<string, any>, exact?: boolean } & T
|
|
351
351
|
) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
|
|
352
352
|
```
|
|
@@ -355,17 +355,17 @@ getAllRows: <T extends { asJSON?: boolean }>(
|
|
|
355
355
|
```typescript
|
|
356
356
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
357
357
|
// 1. Get ALL rows on the current page
|
|
358
|
-
const allRows = await table.
|
|
358
|
+
const allRows = await table.getAllCurrentRows();
|
|
359
359
|
expect(allRows.length).toBeGreaterThan(0);
|
|
360
360
|
|
|
361
361
|
// 2. Get subset of rows (Filtering)
|
|
362
|
-
const tokyoUsers = await table.
|
|
362
|
+
const tokyoUsers = await table.getAllCurrentRows({
|
|
363
363
|
filter: { Office: 'Tokyo' }
|
|
364
364
|
});
|
|
365
365
|
expect(tokyoUsers.length).toBeGreaterThan(0);
|
|
366
366
|
|
|
367
367
|
// 3. Dump data to JSON
|
|
368
|
-
const data = await table.
|
|
368
|
+
const data = await table.getAllCurrentRows({ asJSON: true });
|
|
369
369
|
console.log(data); // [{ Name: "Airi Satou", ... }, ...]
|
|
370
370
|
expect(data.length).toBeGreaterThan(0);
|
|
371
371
|
expect(data[0]).toHaveProperty('Name');
|
|
@@ -376,7 +376,7 @@ Filter rows with exact match:
|
|
|
376
376
|
<!-- embed: get-all-rows-exact -->
|
|
377
377
|
```typescript
|
|
378
378
|
// Get rows with exact match (default is fuzzy/contains match)
|
|
379
|
-
const exactMatches = await table.
|
|
379
|
+
const exactMatches = await table.getAllCurrentRows({
|
|
380
380
|
filter: { Office: 'Tokyo' },
|
|
381
381
|
exact: true // Requires exact string match
|
|
382
382
|
});
|
|
@@ -405,7 +405,7 @@ Basic usage:
|
|
|
405
405
|
```typescript
|
|
406
406
|
// Example from: https://datatables.net/examples/data_sources/dom
|
|
407
407
|
// Quickly grab all text values from the "Office" column
|
|
408
|
-
const offices = await table.getColumnValues('Office');
|
|
408
|
+
const offices = await table.getColumnValues('Office');
|
|
409
409
|
expect(offices).toContain('Tokyo');
|
|
410
410
|
expect(offices.length).toBeGreaterThan(0);
|
|
411
411
|
```
|
|
@@ -560,11 +560,17 @@ A `SmartRow` extends Playwright's `Locator` with table-aware methods.
|
|
|
560
560
|
<!-- embed-type: SmartRow -->
|
|
561
561
|
```typescript
|
|
562
562
|
export type SmartRow = Locator & {
|
|
563
|
+
getRequestIndex(): number | undefined; // Helper to get the row index if known
|
|
564
|
+
rowIndex?: number;
|
|
563
565
|
getCell(column: string): Locator;
|
|
564
566
|
toJSON(): Promise<Record<string, string>>;
|
|
565
567
|
/**
|
|
566
568
|
* Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
|
|
567
569
|
*/
|
|
570
|
+
fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
571
|
+
/**
|
|
572
|
+
* Alias for fill() to avoid conflict with Locator.fill()
|
|
573
|
+
*/
|
|
568
574
|
smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
569
575
|
};
|
|
570
576
|
```
|
|
@@ -583,30 +589,70 @@ Configuration options for `useTable()`.
|
|
|
583
589
|
|
|
584
590
|
<!-- embed-type: TableConfig -->
|
|
585
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
|
+
*/
|
|
586
620
|
export interface TableConfig {
|
|
587
|
-
|
|
588
|
-
headerSelector?:
|
|
589
|
-
|
|
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 */
|
|
590
637
|
pagination?: PaginationStrategy;
|
|
638
|
+
/** Sorting Strategy */
|
|
591
639
|
sorting?: SortingStrategy;
|
|
592
|
-
|
|
593
|
-
/**
|
|
640
|
+
/**
|
|
594
641
|
* Hook to rename columns dynamically.
|
|
595
|
-
* * @param args.text - The default innerText of the header.
|
|
596
|
-
* @param args.index - The column index.
|
|
597
|
-
* @param args.locator - The specific header cell locator.
|
|
598
642
|
*/
|
|
599
643
|
headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
|
|
644
|
+
/** Automatically scroll to table on init */
|
|
600
645
|
autoScroll?: boolean;
|
|
601
|
-
/**
|
|
602
|
-
* Enable debug mode to log internal state to console.
|
|
603
|
-
*/
|
|
646
|
+
/** Enable debug logs */
|
|
604
647
|
debug?: boolean;
|
|
605
|
-
/**
|
|
606
|
-
* Strategy to reset the table to the initial page.
|
|
607
|
-
* Called when table.reset() is invoked.
|
|
608
|
-
*/
|
|
648
|
+
/** Reset hook */
|
|
609
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;
|
|
610
656
|
}
|
|
611
657
|
```
|
|
612
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";
|