@rickcedwhat/playwright-smart-table 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,7 +63,7 @@ console.log(data);
63
63
  **Key Benefits:**
64
64
  - ✅ Column names instead of indices (survives column reordering)
65
65
  - ✅ Extends Playwright's `Locator` API (all `.click()`, `.isVisible()`, etc. work)
66
- - ✅ `.toJSON()` for quick data extraction
66
+ - ✅ `.toJSON()` for quick data extraction (uses `columnStrategy` to ensure visibility)
67
67
 
68
68
  ---
69
69
 
@@ -81,9 +81,11 @@ 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: PaginationStrategies.clickNext(() =>
85
- page.getByRole('link', { name: 'Next' })
86
- ),
84
+ strategies: {
85
+ pagination: Strategies.Pagination.clickNext(() =>
86
+ page.getByRole('link', { name: 'Next' })
87
+ )
88
+ },
87
89
  maxPages: 5 // Allow scanning up to 5 pages
88
90
  });
89
91
  await table.init();
@@ -229,9 +231,11 @@ const table = useTable(page.locator('.MuiDataGrid-root').first(), {
229
231
  rowSelector: '.MuiDataGrid-row',
230
232
  headerSelector: '.MuiDataGrid-columnHeader',
231
233
  cellSelector: '.MuiDataGrid-cell',
232
- pagination: PaginationStrategies.clickNext(
233
- (root) => root.getByRole("button", { name: "Go to next page" })
234
- ),
234
+ strategies: {
235
+ pagination: Strategies.Pagination.clickNext(
236
+ (root) => root.getByRole("button", { name: "Go to next page" })
237
+ )
238
+ },
235
239
  maxPages: 5,
236
240
  // Transform empty columns (detected as __col_0, __col_1, etc.) to meaningful names
237
241
  headerTransformer: ({ text }) => {
@@ -333,6 +337,10 @@ const data = await row.toJSON();
333
337
 
334
338
  expect(data).toHaveProperty('Name', 'Airi Satou');
335
339
  expect(data).toHaveProperty('Position');
340
+
341
+ // Get specific columns only (faster for large tables)
342
+ const partial = await row.toJSON({ columns: ['Name'] });
343
+ expect(partial).toEqual({ Name: 'Airi Satou' });
336
344
  ```
337
345
  <!-- /embed: get-by-row-json -->
338
346
 
@@ -463,32 +471,38 @@ This library uses the **Strategy Pattern** for pagination. Use built-in strategi
463
471
 
464
472
  ### Built-in Strategies
465
473
 
466
- #### <a name="tablestrategiesclicknext"></a>`TableStrategies.clickNext(selector)`
474
+ #### <a name="tablestrategiesclicknext"></a>`Strategies.Pagination.clickNext(selector)`
467
475
 
468
476
  Best for standard paginated tables (Datatables, lists). Clicks a button/link and waits for table content to change.
469
477
 
470
478
  ```typescript
471
- pagination: TableStrategies.clickNext((root) =>
472
- root.page().getByRole('button', { name: 'Next' })
473
- )
479
+ strategies: {
480
+ pagination: Strategies.Pagination.clickNext((root) =>
481
+ root.page().getByRole('button', { name: 'Next' })
482
+ )
483
+ }
474
484
  ```
475
485
 
476
- #### <a name="tablestrategiesinfinitescroll"></a>`TableStrategies.infiniteScroll()`
486
+ #### <a name="tablestrategiesinfinitescroll"></a>`Strategies.Pagination.infiniteScroll()`
477
487
 
478
488
  Best for virtualized grids (AG-Grid, HTMX). Aggressively scrolls to trigger data loading.
479
489
 
480
490
  ```typescript
481
- pagination: TableStrategies.infiniteScroll()
491
+ strategies: {
492
+ pagination: Strategies.Pagination.infiniteScroll()
493
+ }
482
494
  ```
483
495
 
484
- #### <a name="tablestrategiesclickloadmore"></a>`TableStrategies.clickLoadMore(selector)`
496
+ #### <a name="tablestrategiesclickloadmore"></a>`Strategies.Pagination.clickLoadMore(selector)`
485
497
 
486
498
  Best for "Load More" buttons. Clicks and waits for row count to increase.
487
499
 
488
500
  ```typescript
489
- pagination: TableStrategies.clickLoadMore((root) =>
490
- root.getByRole('button', { name: 'Load More' })
491
- )
501
+ strategies: {
502
+ pagination: Strategies.Pagination.clickLoadMore((root) =>
503
+ root.getByRole('button', { name: 'Load More' })
504
+ )
505
+ }
492
506
  ```
493
507
 
494
508
  ### Custom Strategies
@@ -559,19 +573,24 @@ A `SmartRow` extends Playwright's `Locator` with table-aware methods.
559
573
 
560
574
  <!-- embed-type: SmartRow -->
561
575
  ```typescript
562
- export type SmartRow = Locator & {
563
- getRequestIndex(): number | undefined; // Helper to get the row index if known
576
+ export type SmartRow<T = any> = Locator & {
577
+ getRequestIndex(): number | undefined;
564
578
  rowIndex?: number;
565
579
  getCell(column: string): Locator;
566
- toJSON(): Promise<Record<string, string>>;
580
+ toJSON(options?: { columns?: string[] }): Promise<T>;
567
581
  /**
568
- * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
582
+ * Scrolls/paginates to bring this row into view.
583
+ * Only works if rowIndex is known.
569
584
  */
570
- fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
585
+ bringIntoView(): Promise<void>;
571
586
  /**
572
- * Alias for fill() to avoid conflict with Locator.fill()
587
+ * Fills the row with data. Automatically detects input types.
573
588
  */
574
- smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
589
+ fill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;
590
+ /**
591
+ * Alias for fill() to avoid conflict with Locator.fill()
592
+ */
593
+ smartFill: (data: Partial<T> | Record<string, any>, options?: FillOptions) => Promise<void>;
575
594
  };
576
595
  ```
577
596
  <!-- /embed-type: SmartRow -->
@@ -590,29 +609,36 @@ Configuration options for `useTable()`.
590
609
  <!-- embed-type: TableConfig -->
591
610
  ```typescript
592
611
  /**
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;
612
+ * Strategy to filter rows based on criteria.
613
+ */
614
+ export interface FilterStrategy {
615
+ apply(options: {
616
+ rows: Locator;
617
+ filter: { column: string, value: string | RegExp | number };
618
+ colIndex: number;
619
+ tableContext: TableContext;
620
+ }): Locator;
599
621
  }
600
622
 
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';
623
+ /**
624
+ * Organized container for all table interaction strategies.
625
+ */
626
+ export interface TableStrategies {
627
+ /** Strategy for discovering/scanning headers */
628
+ header?: HeaderStrategy;
629
+ /** Strategy for navigating to specific cells (row + column) */
630
+ cellNavigation?: CellNavigationStrategy;
631
+ /** Strategy for filling form inputs */
632
+ fill?: FillStrategy;
633
+ /** Strategy for paginating through data */
634
+ pagination?: PaginationStrategy;
635
+ /** Strategy for sorting columns */
636
+ sorting?: SortingStrategy;
637
+ /** Function to get a cell locator */
638
+ getCellLocator?: GetCellLocatorFn;
639
+ /** Function to get the currently active/focused cell */
640
+ getActiveCell?: GetActiveCellFn;
641
+ }
616
642
 
617
643
  /**
618
644
  * Configuration options for useTable.
@@ -624,22 +650,9 @@ export interface TableConfig {
624
650
  rowSelector?: string;
625
651
  /** Selector for the cells within a row */
626
652
  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
653
  /** Number of pages to scan for verification */
634
654
  maxPages?: number;
635
-
636
- /** Pagination Strategy */
637
- pagination?: PaginationStrategy;
638
- /** Sorting Strategy */
639
- sorting?: SortingStrategy;
640
- /**
641
- * Hook to rename columns dynamically.
642
- */
655
+ /** Hook to rename columns dynamically */
643
656
  headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
644
657
  /** Automatically scroll to table on init */
645
658
  autoScroll?: boolean;
@@ -647,12 +660,8 @@ export interface TableConfig {
647
660
  debug?: boolean;
648
661
  /** Reset hook */
649
662
  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;
663
+ /** All interaction strategies */
664
+ strategies?: TableStrategies;
656
665
  }
657
666
  ```
658
667
  <!-- /embed-type: TableConfig -->
@@ -662,7 +671,7 @@ export interface TableConfig {
662
671
  - `rowSelector`: CSS selector or function for table rows (default: `"tbody tr"`)
663
672
  - `headerSelector`: CSS selector or function for header cells (default: `"th"`)
664
673
  - `cellSelector`: CSS selector or function for data cells (default: `"td"`)
665
- - `pagination`: Strategy function for navigating pages (default: no pagination)
674
+ - `strategies`: Configuration object for interaction strategies (pagination, sorting, etc.)
666
675
  - `maxPages`: Maximum pages to scan when searching (default: `1`)
667
676
  - `headerTransformer`: Function to transform/rename column headers dynamically
668
677
  - `autoScroll`: Automatically scroll table into view (default: `true`)
@@ -676,6 +685,11 @@ Flexible selector type supporting strings, functions, or existing locators.
676
685
  <!-- embed-type: Selector -->
677
686
  ```typescript
678
687
  export type Selector = string | ((root: Locator | Page) => Locator);
688
+
689
+ /**
690
+ * Function to get a cell locator given row, column info.
691
+ * Replaces the old cellResolver.
692
+ */
679
693
  ```
680
694
  <!-- /embed-type: Selector -->
681
695
 
@@ -699,11 +713,70 @@ Function signature for custom pagination logic.
699
713
  export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
700
714
  ```
701
715
  <!-- /embed-type: PaginationStrategy -->
702
-
703
716
  Returns `true` if more data was loaded, `false` if pagination should stop.
704
717
 
705
718
  ---
706
719
 
720
+ ## 🔄 Migration Guide
721
+
722
+ ### Upgrading from v3.x to v4.0
723
+
724
+ **Breaking Change**: Strategy imports are now consolidated under the `Strategies` object.
725
+
726
+ #### Import Changes
727
+ ```typescript
728
+ // ❌ Old (v3.x)
729
+ import { PaginationStrategies, SortingStrategies } from '../src/useTable';
730
+
731
+ // ✅ New (v4.0)
732
+ import { Strategies } from '../src/strategies';
733
+ // or
734
+ import { useTable, Strategies } from '../src/useTable';
735
+ ```
736
+
737
+ #### Strategy Usage
738
+ ```typescript
739
+ // ❌ Old (v3.x)
740
+ strategies: {
741
+ pagination: PaginationStrategies.clickNext(() => page.locator('#next')),
742
+ sorting: SortingStrategies.AriaSort(),
743
+ header: HeaderStrategies.scrollRight,
744
+ cellNavigation: ColumnStrategies.keyboard
745
+ }
746
+
747
+ // ✅ New (v4.0)
748
+ strategies: {
749
+ pagination: Strategies.Pagination.clickNext(() => page.locator('#next')),
750
+ sorting: Strategies.Sorting.AriaSort(),
751
+ header: Strategies.Header.scrollRight,
752
+ cellNavigation: Strategies.Column.keyboard
753
+ }
754
+ ```
755
+
756
+ #### New Features (Optional)
757
+
758
+ **Generic Type Support:**
759
+ ```typescript
760
+ interface User {
761
+ Name: string;
762
+ Email: string;
763
+ Office: string;
764
+ }
765
+
766
+ const table = useTable<User>(page.locator('#table'), config);
767
+ const data = await row.toJSON(); // Type: User (not Record<string, string>)
768
+ ```
769
+
770
+ **Revalidate Method:**
771
+ ```typescript
772
+ // Refresh column mappings when table structure changes
773
+ await table.revalidate();
774
+ ```
775
+
776
+ For detailed migration instructions optimized for AI code transformation, see the [AI Migration Guide](./MIGRATION_v4.md).
777
+
778
+ ---
779
+
707
780
  ## 🚀 Tips & Best Practices
708
781
 
709
782
  1. **Start Simple**: Try the defaults first - they work for most standard HTML tables
@@ -0,0 +1,11 @@
1
+ import { Locator, Page } from "@playwright/test";
2
+ import { FinalTableConfig } from "./types";
3
+ export declare class FilterEngine {
4
+ private config;
5
+ private resolve;
6
+ constructor(config: FinalTableConfig, resolve: (selector: any, parent: Locator | Page) => Locator);
7
+ /**
8
+ * Applies filters to a set of rows.
9
+ */
10
+ applyFilters(baseRows: Locator, filters: Record<string, string | RegExp | number>, map: Map<string, number>, exact: boolean, page: Page): Locator;
11
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FilterEngine = void 0;
4
+ class FilterEngine {
5
+ constructor(config, resolve) {
6
+ this.config = config;
7
+ this.resolve = resolve;
8
+ }
9
+ /**
10
+ * Applies filters to a set of rows.
11
+ */
12
+ applyFilters(baseRows, filters, map, exact, page) {
13
+ let filtered = baseRows;
14
+ // Iterate through each filter criteria
15
+ for (const [colName, value] of Object.entries(filters)) {
16
+ // Find column index
17
+ const colIndex = map.get(colName);
18
+ // TODO: Use ColumnStrategy for better resolution error handling
19
+ if (colIndex === undefined) {
20
+ throw new Error(`Filter Error: Column "${colName}" not found.`);
21
+ }
22
+ const filterVal = typeof value === 'number' ? String(value) : value;
23
+ // Use strategy if provided (For future: configured filter strategies)
24
+ // But for now, we implement the default logic or use custom if we add it to config later
25
+ // Default Filter Logic
26
+ const cellTemplate = this.resolve(this.config.cellSelector, page);
27
+ // This logic assumes 1:1 row-to-cell mapping based on index.
28
+ // filter({ has: ... }) checks if the row *contains* the matching cell.
29
+ // But we need to be specific about WHICH cell.
30
+ // Locator filtering by `has: locator.nth(index)` works if `locator` search is relative to the row.
31
+ filtered = filtered.filter({
32
+ has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
33
+ });
34
+ }
35
+ return filtered;
36
+ }
37
+ }
38
+ exports.FilterEngine = FilterEngine;
@@ -0,0 +1,7 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+ import { SmartRow as SmartRowType, FinalTableConfig, TableResult } from './types';
3
+ /**
4
+ * Factory to create a SmartRow by extending a Playwright Locator.
5
+ * We avoid Class/Proxy to ensure full compatibility with Playwright's expect(locator) matchers.
6
+ */
7
+ export declare const createSmartRow: <T = any>(rowLocator: Locator, map: Map<string, number>, rowIndex: number | undefined, config: FinalTableConfig, rootLocator: Locator, resolve: (item: any, parent: Locator | Page) => Locator, table: TableResult<T> | null) => SmartRowType<T>;
@@ -0,0 +1,155 @@
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.createSmartRow = void 0;
13
+ const fill_1 = require("./strategies/fill");
14
+ /**
15
+ * Factory to create a SmartRow by extending a Playwright Locator.
16
+ * We avoid Class/Proxy to ensure full compatibility with Playwright's expect(locator) matchers.
17
+ */
18
+ const createSmartRow = (rowLocator, map, rowIndex, config, rootLocator, resolve, table) => {
19
+ const smart = rowLocator;
20
+ // Attach State
21
+ smart.rowIndex = rowIndex;
22
+ smart.getRequestIndex = () => rowIndex;
23
+ // Attach Methods
24
+ smart.getCell = (colName) => {
25
+ const idx = map.get(colName);
26
+ if (idx === undefined) {
27
+ const availableColumns = Array.from(map.keys());
28
+ throw new Error(`Column "${colName}" not found. Available: ${availableColumns.join(', ')}`);
29
+ }
30
+ if (config.strategies.getCellLocator) {
31
+ return config.strategies.getCellLocator({
32
+ row: rowLocator,
33
+ columnName: colName,
34
+ columnIndex: idx,
35
+ rowIndex: rowIndex,
36
+ page: rootLocator.page()
37
+ });
38
+ }
39
+ return resolve(config.cellSelector, rowLocator).nth(idx);
40
+ };
41
+ smart.toJSON = (options) => __awaiter(void 0, void 0, void 0, function* () {
42
+ const result = {};
43
+ const page = rootLocator.page();
44
+ for (const [col, idx] of map.entries()) {
45
+ if ((options === null || options === void 0 ? void 0 : options.columns) && !options.columns.includes(col)) {
46
+ continue;
47
+ }
48
+ // Get the cell locator
49
+ const cell = config.strategies.getCellLocator
50
+ ? config.strategies.getCellLocator({
51
+ row: rowLocator,
52
+ columnName: col,
53
+ columnIndex: idx,
54
+ rowIndex: rowIndex,
55
+ page: page
56
+ })
57
+ : resolve(config.cellSelector, rowLocator).nth(idx);
58
+ let targetCell = cell;
59
+ // Check if cell exists
60
+ const count = yield cell.count();
61
+ if (count === 0) {
62
+ // Optimization: Check if we are ALREADY at the target cell
63
+ if (config.strategies.getActiveCell) {
64
+ const active = yield config.strategies.getActiveCell({
65
+ config,
66
+ root: rootLocator,
67
+ page,
68
+ resolve
69
+ });
70
+ if (active && active.rowIndex === rowIndex && active.columnIndex === idx) {
71
+ if (config.debug)
72
+ console.log(`[SmartRow] Already at target cell (r:${active.rowIndex}, c:${active.columnIndex}), skipping navigation.`);
73
+ targetCell = active.locator;
74
+ // Skip navigation and go to reading text
75
+ const text = yield targetCell.innerText();
76
+ result[col] = (text || '').trim();
77
+ continue;
78
+ }
79
+ }
80
+ // Cell doesn't exist - navigate to it
81
+ if (config.debug) {
82
+ console.log(`[SmartRow.toJSON] Cell not found for column "${col}" (index ${idx}), navigating...`);
83
+ }
84
+ yield config.strategies.cellNavigation({
85
+ config: config,
86
+ root: rootLocator,
87
+ page: page,
88
+ resolve: resolve,
89
+ column: col,
90
+ index: idx,
91
+ rowLocator: rowLocator,
92
+ rowIndex: rowIndex
93
+ });
94
+ // Optimization: check if we can get the active cell directly
95
+ if (config.strategies.getActiveCell) {
96
+ const activeCell = yield config.strategies.getActiveCell({
97
+ config,
98
+ root: rootLocator,
99
+ page,
100
+ resolve
101
+ });
102
+ if (activeCell) {
103
+ if (config.debug) {
104
+ console.log(`[SmartRow.toJSON] switching to active cell locator (r:${activeCell.rowIndex}, c:${activeCell.columnIndex})`);
105
+ }
106
+ targetCell = activeCell.locator;
107
+ }
108
+ }
109
+ }
110
+ const text = yield targetCell.innerText();
111
+ result[col] = (text || '').trim();
112
+ }
113
+ return result;
114
+ });
115
+ // @ts-ignore
116
+ smart.fill = (data, fillOptions) => __awaiter(void 0, void 0, void 0, function* () {
117
+ for (const [colName, value] of Object.entries(data)) {
118
+ const colIdx = map.get(colName);
119
+ if (colIdx === undefined) {
120
+ throw new Error(`Column "${colName}" not found in fill data.`);
121
+ }
122
+ yield config.strategies.cellNavigation({
123
+ config: config,
124
+ root: rootLocator,
125
+ page: rootLocator.page(),
126
+ resolve: resolve,
127
+ column: colName,
128
+ index: colIdx,
129
+ rowLocator: rowLocator,
130
+ rowIndex: rowIndex
131
+ });
132
+ const strategy = config.strategies.fill || fill_1.FillStrategies.default;
133
+ yield strategy({
134
+ row: smart,
135
+ columnName: colName,
136
+ value,
137
+ index: rowIndex !== null && rowIndex !== void 0 ? rowIndex : -1,
138
+ page: rowLocator.page(),
139
+ rootLocator,
140
+ table: table,
141
+ fillOptions
142
+ });
143
+ }
144
+ });
145
+ smart.bringIntoView = () => __awaiter(void 0, void 0, void 0, function* () {
146
+ if (rowIndex === undefined) {
147
+ throw new Error('Cannot bring row into view - row index is unknown. Use getByRowIndex() instead of getByRow().');
148
+ }
149
+ // Scroll row into view using Playwright's built-in method
150
+ yield rowLocator.scrollIntoViewIfNeeded();
151
+ });
152
+ smart.smartFill = smart.fill;
153
+ return smart;
154
+ };
155
+ exports.createSmartRow = createSmartRow;
@@ -1,14 +1,36 @@
1
1
  import { StrategyContext } from '../types';
2
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.
3
+ * Defines the contract for a cell navigation strategy.
4
+ * It is responsible for ensuring a specific CELL is visible/focused (navigates to row + column),
5
+ * typically by scrolling or using keyboard navigation.
6
6
  */
7
- export type ColumnStrategy = (context: StrategyContext & {
7
+ export type CellNavigationStrategy = (context: StrategyContext & {
8
8
  column: string;
9
9
  index: number;
10
10
  rowIndex?: number;
11
11
  }) => Promise<void>;
12
+ /** @deprecated Use CellNavigationStrategy instead */
13
+ export type ColumnStrategy = CellNavigationStrategy;
14
+ export declare const CellNavigationStrategies: {
15
+ /**
16
+ * Default strategy: Assumes column is accessible or standard scrolling works.
17
+ * No specific action taken other than what Playwright's default locator handling does.
18
+ */
19
+ default: () => Promise<void>;
20
+ /**
21
+ * Strategy that clicks into the table to establish focus and then uses the Right Arrow key
22
+ * to navigate to the target CELL (navigates down to the row, then right to the column).
23
+ *
24
+ * Useful for canvas-based grids like Glide where DOM scrolling might not be enough for interaction
25
+ * or where keyboard navigation is the primary way to move focus.
26
+ */
27
+ keyboard: (context: StrategyContext & {
28
+ column: string;
29
+ index: number;
30
+ rowIndex?: number;
31
+ }) => Promise<void>;
32
+ };
33
+ /** @deprecated Use CellNavigationStrategies instead */
12
34
  export declare const ColumnStrategies: {
13
35
  /**
14
36
  * Default strategy: Assumes column is accessible or standard scrolling works.
@@ -17,7 +39,7 @@ export declare const ColumnStrategies: {
17
39
  default: () => Promise<void>;
18
40
  /**
19
41
  * Strategy that clicks into the table to establish focus and then uses the Right Arrow key
20
- * to navigate to the target column index.
42
+ * to navigate to the target CELL (navigates down to the row, then right to the column).
21
43
  *
22
44
  * Useful for canvas-based grids like Glide where DOM scrolling might not be enough for interaction
23
45
  * or where keyboard navigation is the primary way to move focus.
@@ -9,8 +9,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.ColumnStrategies = void 0;
13
- exports.ColumnStrategies = {
12
+ exports.ColumnStrategies = exports.CellNavigationStrategies = void 0;
13
+ exports.CellNavigationStrategies = {
14
14
  /**
15
15
  * Default strategy: Assumes column is accessible or standard scrolling works.
16
16
  * No specific action taken other than what Playwright's default locator handling does.
@@ -20,7 +20,7 @@ exports.ColumnStrategies = {
20
20
  }),
21
21
  /**
22
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.
23
+ * to navigate to the target CELL (navigates down to the row, then right to the column).
24
24
  *
25
25
  * Useful for canvas-based grids like Glide where DOM scrolling might not be enough for interaction
26
26
  * or where keyboard navigation is the primary way to move focus.
@@ -30,15 +30,14 @@ exports.ColumnStrategies = {
30
30
  if (typeof rowIndex !== 'number') {
31
31
  throw new Error('Row index is required for keyboard navigation');
32
32
  }
33
- console.log(`[ColumnStrat:keyboard] Using Row Index Navigation: Row ${rowIndex}, Col ${index}`);
34
33
  yield root.focus();
35
- yield page.waitForTimeout(200);
34
+ yield page.waitForTimeout(100);
36
35
  // Robust Navigation:
37
36
  // 1. Jump to Top-Left (Reset) - Sequence for Cross-OS (Mac/Windows)
38
37
  yield page.keyboard.press('Control+Home');
39
38
  yield page.keyboard.press('Meta+ArrowUp'); // Mac Go-To-Top
40
39
  yield page.keyboard.press('Home'); // Ensure start of row
41
- yield page.waitForTimeout(300);
40
+ yield page.waitForTimeout(150);
42
41
  // 2. Move Down to Target Row
43
42
  for (let i = 0; i < rowIndex; i++) {
44
43
  yield page.keyboard.press('ArrowDown');
@@ -47,7 +46,9 @@ exports.ColumnStrategies = {
47
46
  for (let i = 0; i < index; i++) {
48
47
  yield page.keyboard.press('ArrowRight');
49
48
  }
50
- yield page.waitForTimeout(100);
51
- yield page.waitForTimeout(100);
49
+ yield page.waitForTimeout(50);
52
50
  })
53
51
  };
52
+ // Backwards compatibility - deprecated
53
+ /** @deprecated Use CellNavigationStrategies instead */
54
+ exports.ColumnStrategies = exports.CellNavigationStrategies;
@@ -63,7 +63,6 @@ exports.HeaderStrategies = {
63
63
  yield scrollerHandle.evaluate((el, amount) => el.scrollLeft += amount, scrollAmount);
64
64
  yield page.waitForTimeout(300);
65
65
  const newHeaders = yield getVisible();
66
- console.log(`[HeaderStrat:scrollRight] Scrolled ${scrollAmount}, found: ${newHeaders.length} visible.`);
67
66
  newHeaders.forEach(h => collectedHeaders.add(h));
68
67
  if (collectedHeaders.size === sizeBefore) {
69
68
  yield scrollerHandle.evaluate((el, amount) => el.scrollLeft += amount, scrollAmount);
@@ -115,7 +114,8 @@ exports.HeaderStrategies = {
115
114
  // Reset to home
116
115
  yield page.keyboard.press('Control+Home');
117
116
  yield page.keyboard.press('Home');
118
- yield page.waitForTimeout(200);
117
+ // Wait for potential scroll/focus reset
118
+ yield page.evaluate(() => new Promise(requestAnimationFrame));
119
119
  currentHeaders = yield getVisible();
120
120
  currentHeaders.forEach(h => collectedHeaders.add(h));
121
121
  // 3. Navigate right loop
@@ -123,9 +123,9 @@ exports.HeaderStrategies = {
123
123
  for (let i = 0; i < limit; i++) {
124
124
  const sizeBefore = collectedHeaders.size;
125
125
  yield page.keyboard.press('ArrowRight');
126
- yield page.waitForTimeout(100);
126
+ // Small breathing room for key press to register
127
+ yield page.evaluate(() => new Promise(requestAnimationFrame));
127
128
  const newHeaders = yield getVisible();
128
- console.log(`[HeaderStrat:keyboard] Step ${i}, found visible: ${newHeaders}`);
129
129
  newHeaders.forEach(h => collectedHeaders.add(h));
130
130
  if (collectedHeaders.size === sizeBefore) {
131
131
  silentCounter++;