@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 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
@@ -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: TableStrategies.clickNext(
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="getallrows"></a>`getAllRows(options?)`
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
- **Purpose:** Inclusive retrieval - gets all rows matching optional filters.
343
+ **Best for:** Checking existence, validating sort order, bulk data extraction on the current page.
344
344
 
345
- **Best for:** Checking existence, validating sort order, bulk data extraction.
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
- getAllRows: <T extends { asJSON?: boolean }>(
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.getAllRows();
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.getAllRows({
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.getAllRows({ asJSON: true });
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.getAllRows({
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
- rowSelector?: Selector;
588
- headerSelector?: Selector;
589
- 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 */
590
637
  pagination?: PaginationStrategy;
638
+ /** Sorting Strategy */
591
639
  sorting?: SortingStrategy;
592
- maxPages?: number;
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
+ };
@@ -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: (\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean }\n ) => SmartRow;\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 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'>;\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";