@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 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: TableStrategies.clickNext(() =>
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 getByRowAcrossPages for pagination
95
- await expect(await table.getByRowAcrossPages({ Name: "Colleen Hurst" })).toBeVisible();
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.getByRowAcrossPages({ Name: 'Angelica Ramos' });
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: TableStrategies.clickNext(
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.getByRowAcrossPages({ "Last name": "Melisandre" });
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 directly as JSON object
332
- const data = await table.getByRow({ Name: 'Airi Satou' }, { asJSON: true });
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="getallrows"></a>`getAllRows(options?)`
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
- getAllRows: <T extends { asJSON?: boolean }>(
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.getAllRows();
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.getAllRows({
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.getAllRows({ asJSON: true });
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.getAllRows({
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
- rowSelector?: Selector;
587
- headerSelector?: Selector;
588
- cellSelector?: Selector;
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
- maxPages?: number;
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
+ };
@@ -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 rowSelector?: Selector;\n headerSelector?: Selector;\n cellSelector?: Selector;\n pagination?: PaginationStrategy;\n sorting?: SortingStrategy;\n maxPages?: number;\n /**\n * Hook to rename columns dynamically.\n * * @param args.text - The default innerText of the header.\n * @param args.index - The column index.\n * @param args.locator - The specific header cell locator.\n */\n headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n autoScroll?: boolean;\n /**\n * Enable debug mode to log internal state to console.\n */\n debug?: boolean;\n /**\n * Strategy to reset the table to the initial page.\n * Called when table.reset() is invoked.\n */\n onReset?: (context: TableContext) => Promise<void>;\n}\n\n/**\n * Represents the final, resolved table configuration after default values have been applied.\n * All optional properties from TableConfig are now required, except for `sorting`.\n */\nexport type FinalTableConfig = Required<Omit<TableConfig, 'sorting'>> & {\n sorting?: SortingStrategy;\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 * Columns not specified here will use auto-detection.\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: <T extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean } & T\n ) => T['asJSON'] extends true ? Promise<Record<string, string>> : SmartRow;\n\n /**\n * Finds a row across multiple pages using pagination. Auto-initializes if needed.\n */\n getByRowAcrossPages: <T extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean, maxPages?: number } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;\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, 'getByRowAcrossPages' | 'iterateThroughTable' | 'reset'>;\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";