@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 +140 -67
- package/dist/filterEngine.d.ts +11 -0
- package/dist/filterEngine.js +38 -0
- package/dist/smartRow.d.ts +7 -0
- package/dist/smartRow.js +155 -0
- package/dist/strategies/columns.d.ts +27 -5
- package/dist/strategies/columns.js +9 -8
- package/dist/strategies/headers.js +4 -4
- package/dist/strategies/index.d.ts +53 -0
- package/dist/strategies/index.js +22 -1
- package/dist/strategies/pagination.d.ts +1 -1
- package/dist/strategies/pagination.js +2 -2
- package/dist/strategies/resolution.d.ts +22 -0
- package/dist/strategies/resolution.js +30 -0
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +104 -44
- package/dist/types.d.ts +97 -51
- package/dist/useTable.d.ts +7 -4
- package/dist/useTable.js +93 -244
- package/package.json +1 -1
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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>`
|
|
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
|
-
|
|
472
|
-
|
|
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>`
|
|
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
|
-
|
|
491
|
+
strategies: {
|
|
492
|
+
pagination: Strategies.Pagination.infiniteScroll()
|
|
493
|
+
}
|
|
482
494
|
```
|
|
483
495
|
|
|
484
|
-
#### <a name="tablestrategiesclickloadmore"></a>`
|
|
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
|
-
|
|
490
|
-
|
|
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;
|
|
576
|
+
export type SmartRow<T = any> = Locator & {
|
|
577
|
+
getRequestIndex(): number | undefined;
|
|
564
578
|
rowIndex?: number;
|
|
565
579
|
getCell(column: string): Locator;
|
|
566
|
-
toJSON(): Promise<
|
|
580
|
+
toJSON(options?: { columns?: string[] }): Promise<T>;
|
|
567
581
|
/**
|
|
568
|
-
*
|
|
582
|
+
* Scrolls/paginates to bring this row into view.
|
|
583
|
+
* Only works if rowIndex is known.
|
|
569
584
|
*/
|
|
570
|
-
|
|
585
|
+
bringIntoView(): Promise<void>;
|
|
571
586
|
/**
|
|
572
|
-
*
|
|
587
|
+
* Fills the row with data. Automatically detects input types.
|
|
573
588
|
*/
|
|
574
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
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>;
|
package/dist/smartRow.js
ADDED
|
@@ -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
|
|
4
|
-
* It is responsible for ensuring a specific
|
|
5
|
-
* typically by scrolling or
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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++;
|