@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 +173 -54
- 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 +52 -0
- package/dist/strategies/columns.js +54 -0
- package/dist/strategies/fill.d.ts +7 -0
- package/dist/strategies/fill.js +88 -0
- package/dist/strategies/headers.d.ts +29 -0
- package/dist/strategies/headers.js +142 -0
- 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 +152 -35
- package/dist/types.d.ts +153 -33
- package/dist/useTable.d.ts +10 -9
- package/dist/useTable.js +135 -283
- 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();
|
|
@@ -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
|
-
|
|
235
|
-
|
|
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="
|
|
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
|
-
**
|
|
351
|
+
**Best for:** Checking existence, validating sort order, bulk data extraction on the current page.
|
|
344
352
|
|
|
345
|
-
**
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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>`
|
|
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,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<
|
|
580
|
+
toJSON(options?: { columns?: string[] }): Promise<T>;
|
|
565
581
|
/**
|
|
566
|
-
*
|
|
582
|
+
* Scrolls/paginates to bring this row into view.
|
|
583
|
+
* Only works if rowIndex is known.
|
|
567
584
|
*/
|
|
568
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
- `
|
|
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>;
|
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;
|
|
@@ -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
|
+
};
|