@rickcedwhat/playwright-smart-table 3.1.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: TableStrategies.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();
@@ -127,10 +129,10 @@ If your tests navigate deep into a paginated table, use `.reset()` to return to
127
129
  // Navigate deep into the table by searching for a row on a later page
128
130
  try {
129
131
  await table.searchForRow({ Name: 'Angelica Ramos' });
130
- } catch (e) {}
132
+ } catch (e) { }
131
133
 
132
134
  // Reset internal state (and potentially UI) to initial page
133
- await table.reset();
135
+ await table.reset();
134
136
  await table.init(); // Re-init after reset
135
137
 
136
138
  // Now subsequent searches start from the beginning
@@ -147,7 +149,7 @@ Efficiently extract all values from a specific column:
147
149
  ```typescript
148
150
  // Example from: https://datatables.net/examples/data_sources/dom
149
151
  // Quickly grab all text values from the "Office" column
150
- const offices = await table.getColumnValues('Office');
152
+ const offices = await table.getColumnValues('Office');
151
153
  expect(offices).toContain('Tokyo');
152
154
  expect(offices.length).toBeGreaterThan(0);
153
155
  ```
@@ -193,8 +195,6 @@ For edge cases where auto-detection doesn't work (e.g., custom components, multi
193
195
 
194
196
  <!-- embed: fill-custom-mappers -->
195
197
  ```typescript
196
- const row = table.getByRow({ ID: '1' });
197
-
198
198
  // Use custom input mappers for specific columns
199
199
  await row.smartFill({
200
200
  Name: 'John Updated',
@@ -231,9 +231,11 @@ const table = useTable(page.locator('.MuiDataGrid-root').first(), {
231
231
  rowSelector: '.MuiDataGrid-row',
232
232
  headerSelector: '.MuiDataGrid-columnHeader',
233
233
  cellSelector: '.MuiDataGrid-cell',
234
- pagination: TableStrategies.clickNext(
235
- (root) => root.getByRole("button", { name: "Go to next page" })
236
- ),
234
+ strategies: {
235
+ pagination: Strategies.Pagination.clickNext(
236
+ (root) => root.getByRole("button", { name: "Go to next page" })
237
+ )
238
+ },
237
239
  maxPages: 5,
238
240
  // Transform empty columns (detected as __col_0, __col_1, etc.) to meaningful names
239
241
  headerTransformer: ({ text }) => {
@@ -335,18 +337,24 @@ const data = await row.toJSON();
335
337
 
336
338
  expect(data).toHaveProperty('Name', 'Airi Satou');
337
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' });
338
344
  ```
339
345
  <!-- /embed: get-by-row-json -->
340
346
 
341
- #### <a name="getallrows"></a>`getAllRows(options?)`
347
+ #### <a name="getallcurrentrows"></a>`getAllCurrentRows(options?)`
348
+
349
+ **Purpose:** Inclusive retrieval - gets all rows on the current page matching optional filters.
342
350
 
343
- **Purpose:** Inclusive retrieval - gets all rows matching optional filters.
351
+ **Best for:** Checking existence, validating sort order, bulk data extraction on the current page.
344
352
 
345
- **Best for:** Checking existence, validating sort order, bulk data extraction.
353
+ > **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
354
 
347
355
  **Type Signature:**
348
356
  ```typescript
349
- getAllRows: <T extends { asJSON?: boolean }>(
357
+ getAllCurrentRows: <T extends { asJSON?: boolean }>(
350
358
  options?: { filter?: Record<string, any>, exact?: boolean } & T
351
359
  ) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
352
360
  ```
@@ -355,17 +363,17 @@ getAllRows: <T extends { asJSON?: boolean }>(
355
363
  ```typescript
356
364
  // Example from: https://datatables.net/examples/data_sources/dom
357
365
  // 1. Get ALL rows on the current page
358
- const allRows = await table.getAllRows();
366
+ const allRows = await table.getAllCurrentRows();
359
367
  expect(allRows.length).toBeGreaterThan(0);
360
368
 
361
369
  // 2. Get subset of rows (Filtering)
362
- const tokyoUsers = await table.getAllRows({
370
+ const tokyoUsers = await table.getAllCurrentRows({
363
371
  filter: { Office: 'Tokyo' }
364
372
  });
365
373
  expect(tokyoUsers.length).toBeGreaterThan(0);
366
374
 
367
375
  // 3. Dump data to JSON
368
- const data = await table.getAllRows({ asJSON: true });
376
+ const data = await table.getAllCurrentRows({ asJSON: true });
369
377
  console.log(data); // [{ Name: "Airi Satou", ... }, ...]
370
378
  expect(data.length).toBeGreaterThan(0);
371
379
  expect(data[0]).toHaveProperty('Name');
@@ -376,7 +384,7 @@ Filter rows with exact match:
376
384
  <!-- embed: get-all-rows-exact -->
377
385
  ```typescript
378
386
  // Get rows with exact match (default is fuzzy/contains match)
379
- const exactMatches = await table.getAllRows({
387
+ const exactMatches = await table.getAllCurrentRows({
380
388
  filter: { Office: 'Tokyo' },
381
389
  exact: true // Requires exact string match
382
390
  });
@@ -405,7 +413,7 @@ Basic usage:
405
413
  ```typescript
406
414
  // Example from: https://datatables.net/examples/data_sources/dom
407
415
  // Quickly grab all text values from the "Office" column
408
- const offices = await table.getColumnValues('Office');
416
+ const offices = await table.getColumnValues('Office');
409
417
  expect(offices).toContain('Tokyo');
410
418
  expect(offices.length).toBeGreaterThan(0);
411
419
  ```
@@ -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,13 +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 & {
576
+ export type SmartRow<T = any> = Locator & {
577
+ getRequestIndex(): number | undefined;
578
+ rowIndex?: number;
563
579
  getCell(column: string): Locator;
564
- toJSON(): Promise<Record<string, string>>;
580
+ toJSON(options?: { columns?: string[] }): Promise<T>;
565
581
  /**
566
- * 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.
567
584
  */
568
- smartFill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
585
+ bringIntoView(): Promise<void>;
586
+ /**
587
+ * Fills the row with data. Automatically detects input types.
588
+ */
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>;
569
594
  };
570
595
  ```
571
596
  <!-- /embed-type: SmartRow -->
@@ -583,30 +608,60 @@ Configuration options for `useTable()`.
583
608
 
584
609
  <!-- embed-type: TableConfig -->
585
610
  ```typescript
586
- export interface TableConfig {
587
- rowSelector?: Selector;
588
- headerSelector?: Selector;
589
- cellSelector?: Selector;
611
+ /**
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;
621
+ }
622
+
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 */
590
634
  pagination?: PaginationStrategy;
635
+ /** Strategy for sorting columns */
591
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
+ }
642
+
643
+ /**
644
+ * Configuration options for useTable.
645
+ */
646
+ export interface TableConfig {
647
+ /** Selector for the table headers */
648
+ headerSelector?: string;
649
+ /** Selector for the table rows */
650
+ rowSelector?: string;
651
+ /** Selector for the cells within a row */
652
+ cellSelector?: string;
653
+ /** Number of pages to scan for verification */
592
654
  maxPages?: number;
593
- /**
594
- * 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
- */
655
+ /** Hook to rename columns dynamically */
599
656
  headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;
657
+ /** Automatically scroll to table on init */
600
658
  autoScroll?: boolean;
601
- /**
602
- * Enable debug mode to log internal state to console.
603
- */
659
+ /** Enable debug logs */
604
660
  debug?: boolean;
605
- /**
606
- * Strategy to reset the table to the initial page.
607
- * Called when table.reset() is invoked.
608
- */
661
+ /** Reset hook */
609
662
  onReset?: (context: TableContext) => Promise<void>;
663
+ /** All interaction strategies */
664
+ strategies?: TableStrategies;
610
665
  }
611
666
  ```
612
667
  <!-- /embed-type: TableConfig -->
@@ -616,7 +671,7 @@ export interface TableConfig {
616
671
  - `rowSelector`: CSS selector or function for table rows (default: `"tbody tr"`)
617
672
  - `headerSelector`: CSS selector or function for header cells (default: `"th"`)
618
673
  - `cellSelector`: CSS selector or function for data cells (default: `"td"`)
619
- - `pagination`: Strategy function for navigating pages (default: no pagination)
674
+ - `strategies`: Configuration object for interaction strategies (pagination, sorting, etc.)
620
675
  - `maxPages`: Maximum pages to scan when searching (default: `1`)
621
676
  - `headerTransformer`: Function to transform/rename column headers dynamically
622
677
  - `autoScroll`: Automatically scroll table into view (default: `true`)
@@ -630,6 +685,11 @@ Flexible selector type supporting strings, functions, or existing locators.
630
685
  <!-- embed-type: Selector -->
631
686
  ```typescript
632
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
+ */
633
693
  ```
634
694
  <!-- /embed-type: Selector -->
635
695
 
@@ -653,11 +713,70 @@ Function signature for custom pagination logic.
653
713
  export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
654
714
  ```
655
715
  <!-- /embed-type: PaginationStrategy -->
656
-
657
716
  Returns `true` if more data was loaded, `false` if pagination should stop.
658
717
 
659
718
  ---
660
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
+
661
780
  ## 🚀 Tips & Best Practices
662
781
 
663
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;
@@ -0,0 +1,52 @@
1
+ import { StrategyContext } from '../types';
2
+ /**
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
+ */
7
+ export type CellNavigationStrategy = (context: StrategyContext & {
8
+ column: string;
9
+ index: number;
10
+ rowIndex?: number;
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 */
34
+ export declare const ColumnStrategies: {
35
+ /**
36
+ * Default strategy: Assumes column is accessible or standard scrolling works.
37
+ * No specific action taken other than what Playwright's default locator handling does.
38
+ */
39
+ default: () => Promise<void>;
40
+ /**
41
+ * Strategy that clicks into the table to establish focus and then uses the Right Arrow key
42
+ * to navigate to the target CELL (navigates down to the row, then right to the column).
43
+ *
44
+ * Useful for canvas-based grids like Glide where DOM scrolling might not be enough for interaction
45
+ * or where keyboard navigation is the primary way to move focus.
46
+ */
47
+ keyboard: (context: StrategyContext & {
48
+ column: string;
49
+ index: number;
50
+ rowIndex?: number;
51
+ }) => Promise<void>;
52
+ };