@rickcedwhat/playwright-smart-table 6.18.0 → 6.19.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/dist/engine/rowFinder.d.ts +10 -0
- package/dist/engine/rowFinder.js +25 -57
- package/dist/engine/rowResolution.d.ts +28 -0
- package/dist/engine/rowResolution.js +73 -0
- package/dist/engine/tableIteration.d.ts +3 -1
- package/dist/engine/tableIteration.js +51 -45
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/plugins/index.d.ts +3 -0
- package/dist/presets/mui.js +25 -0
- package/dist/smartRow.js +172 -11
- package/dist/strategies/fill.js +5 -1
- package/dist/strategies/viewport.js +46 -3
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +98 -8
- package/dist/types.d.ts +91 -6
- package/dist/useTable.js +100 -6
- package/package.json +1 -1
package/dist/typeContext.js
CHANGED
|
@@ -167,6 +167,16 @@ export interface ViewportStrategy {
|
|
|
167
167
|
*/
|
|
168
168
|
getVisibleRowRange?: (context: TableContext) => Promise<{ first: number; last: number }>;
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Returns the DOM positions (0-based, document order — indices into the resolved
|
|
172
|
+
* \`rowSelector\` set) of rows currently within the scroll container's visible bounds.
|
|
173
|
+
* Unlike \`getVisibleRowRange\` (a logical min/max), this identifies the exact rows, so
|
|
174
|
+
* \`map\`/\`forEach\`/\`filter\` can skip overscan rows during collection (see #353 / #357).
|
|
175
|
+
* Geometry-based and inclusive (any overlap counts as visible). When the container can't
|
|
176
|
+
* be measured, return all positions so no rows are filtered.
|
|
177
|
+
*/
|
|
178
|
+
getVisibleRowIndices?: (context: TableContext) => Promise<number[]>;
|
|
179
|
+
|
|
170
180
|
/**
|
|
171
181
|
* Returns the 0-based index range of columns currently rendered in the DOM.
|
|
172
182
|
* Used to detect when a target column is not yet mounted before reading.
|
|
@@ -254,8 +264,14 @@ export type SmartRow<T = any> = Locator & {
|
|
|
254
264
|
*
|
|
255
265
|
* const partial = await row.toJSON({ columns: ['Name', 'Email'] });
|
|
256
266
|
* // { Name: 'John', Email: 'john@example.com' }
|
|
267
|
+
*
|
|
268
|
+
* // Atomic: snapshot all cell values in a single evaluate — zero inter-column stagger.
|
|
269
|
+
* // Requires cellSelector to be a CSS string (not a function).
|
|
270
|
+
* // Column overrides ARE supported (run against a frozen off-screen reconstruction).
|
|
271
|
+
* // Uses textContent (not layout-dependent innerText) for non-override columns.
|
|
272
|
+
* const coherent = await row.toJSON({ atomic: true });
|
|
257
273
|
*/
|
|
258
|
-
toJSON(options?: { columns?: string[] }): Promise<T>;
|
|
274
|
+
toJSON(options?: { columns?: string[]; atomic?: boolean }): Promise<T>;
|
|
259
275
|
|
|
260
276
|
/**
|
|
261
277
|
* Scrolls/paginates to bring this row into view.
|
|
@@ -424,12 +440,43 @@ export type FillStrategy = (options: {
|
|
|
424
440
|
fillOptions?: FillOptions;
|
|
425
441
|
}) => Promise<void>;
|
|
426
442
|
|
|
443
|
+
/** Context passed as the second argument to {@link ColumnOverride.read}. */
|
|
444
|
+
export interface ColumnOverrideReadContext {
|
|
445
|
+
/**
|
|
446
|
+
* The parent row. Use for multi-cell or row-derived values — e.g. a synthetic column
|
|
447
|
+
* that reads an \`a[href]\` or a \`data-*\` attribute from the row rather than a cell:
|
|
448
|
+
* \`read: (_cell, { row }) => row.evaluate(el => el.querySelector('a')?.href)\`.
|
|
449
|
+
*/
|
|
450
|
+
row: SmartRow;
|
|
451
|
+
/** The column being read. */
|
|
452
|
+
columnName: string;
|
|
453
|
+
/** The column's 0-based index in the resolved header map. */
|
|
454
|
+
columnIndex: number;
|
|
455
|
+
/**
|
|
456
|
+
* Get a Locator for another cell in the same row by column name.
|
|
457
|
+
* Returns raw cell Locators, not override-processed values.
|
|
458
|
+
*
|
|
459
|
+
* In atomic mode, the Locator points at the frozen reconstructed cell — coherent with
|
|
460
|
+
* every other cell from the same snapshot. In non-atomic mode, it points at the live cell.
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* read: async (_cell, { getCell }) => {
|
|
464
|
+
* const name = (await getCell('Name').innerText()).trim();
|
|
465
|
+
* const href = await getCell('Name').locator('a').getAttribute('href') || '';
|
|
466
|
+
* return \`\${name} | \${href}\`;
|
|
467
|
+
* }
|
|
468
|
+
*/
|
|
469
|
+
getCell: (columnName: string) => Locator;
|
|
470
|
+
}
|
|
471
|
+
|
|
427
472
|
export interface ColumnOverride<TValue = any> {
|
|
428
|
-
/**
|
|
473
|
+
/**
|
|
429
474
|
* How to extract the value from the cell.
|
|
430
|
-
* \`context\` provides
|
|
475
|
+
* The second \`context\` argument provides the parent \`row\` (for multi-cell or row-derived
|
|
476
|
+
* values), plus \`columnName\` and \`columnIndex\`. Backwards-compatible: existing
|
|
477
|
+
* single-argument \`read(cell)\` implementations keep working.
|
|
431
478
|
*/
|
|
432
|
-
read?: (cell: Locator) => Promise<TValue> | TValue;
|
|
479
|
+
read?: (cell: Locator, context: ColumnOverrideReadContext) => Promise<TValue> | TValue;
|
|
433
480
|
|
|
434
481
|
/**
|
|
435
482
|
* How to fill the cell with a new value. (Replaces smartFill default logic)
|
|
@@ -620,6 +667,13 @@ export interface TableConfig<T = any> {
|
|
|
620
667
|
* Overrides both default extraction (toJSON) and filling (smartFill) logic.
|
|
621
668
|
*/
|
|
622
669
|
columnOverrides?: Partial<Record<keyof T, ColumnOverride<T[keyof T]>>>;
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Locator for an empty-state element that replaces the table when there are no results.
|
|
673
|
+
* If header resolution fails during init() and this locator is visible, init() succeeds
|
|
674
|
+
* and isEmpty() returns true. All row operations still throw normally.
|
|
675
|
+
*/
|
|
676
|
+
emptyState?: Locator;
|
|
623
677
|
}
|
|
624
678
|
|
|
625
679
|
export interface FinalTableConfig<T = any> extends TableConfig<T> {
|
|
@@ -649,9 +703,17 @@ export interface FillOptions {
|
|
|
649
703
|
/** Callback context passed to forEach, map, and filter. */
|
|
650
704
|
export type RowIterationContext<T = any> = {
|
|
651
705
|
row: SmartRow<T>;
|
|
652
|
-
/**
|
|
706
|
+
/**
|
|
707
|
+
* The row's logical/data-model index. When a \`resolveRowIndex\` strategy is configured
|
|
708
|
+
* (e.g. MUI DataGrid's \`data-rowindex\`) this is the grid's true row index; otherwise it
|
|
709
|
+
* equals \`index\`. Use this for \`row.bringIntoView()\` and position math on virtualized
|
|
710
|
+
* tables — it is stable across scrolling/dedupe, unlike the visit-order \`index\`.
|
|
711
|
+
*/
|
|
653
712
|
rowIndex: number;
|
|
654
|
-
/**
|
|
713
|
+
/**
|
|
714
|
+
* 0-based enumeration counter — the order this row was visited (contiguous within the run).
|
|
715
|
+
* Not a DOM position or grid identity. Use \`rowIndex\` for the row's data-model index.
|
|
716
|
+
*/
|
|
655
717
|
index: number;
|
|
656
718
|
/** 0-based page index — which page this row was collected from. */
|
|
657
719
|
pageIndex: number;
|
|
@@ -704,6 +766,13 @@ export interface TableResult<T = any> extends AsyncIterable<{ row: SmartRow<T>;
|
|
|
704
766
|
*/
|
|
705
767
|
isInitialized(): boolean;
|
|
706
768
|
|
|
769
|
+
/**
|
|
770
|
+
* SYNC: Returns true if init() resolved via the emptyState path — the table's
|
|
771
|
+
* empty-state locator was visible when header resolution failed.
|
|
772
|
+
* Row operations still throw normally; use this to branch before calling them.
|
|
773
|
+
*/
|
|
774
|
+
isEmpty(): boolean;
|
|
775
|
+
|
|
707
776
|
getHeaders: () => Promise<string[]>;
|
|
708
777
|
getHeaderCell: (columnName: string) => Promise<Locator>;
|
|
709
778
|
|
|
@@ -726,8 +795,9 @@ export interface TableResult<T = any> extends AsyncIterable<{ row: SmartRow<T>;
|
|
|
726
795
|
* For button-paginated tables this is the i-th row on the current page. For virtualized
|
|
727
796
|
* tables the render window shifts as you scroll, so \`getRowByIndex(i)\` returns whatever
|
|
728
797
|
* row currently sits at DOM position \`i\` — NOT the logical/absolute row \`i\` in the dataset
|
|
729
|
-
* once the list has scrolled.
|
|
730
|
-
* \`
|
|
798
|
+
* once the list has scrolled. For the row with a specific logical/data-model index on a
|
|
799
|
+
* virtualized table, use the async \`findRowByIndex(i)\` instead (it scrolls to the row);
|
|
800
|
+
* to iterate by logical identity, use \`map\` / \`findRows\` (with a \`dedupe\` strategy).
|
|
731
801
|
*
|
|
732
802
|
* Resolution is lazy: an out-of-range index yields a SmartRow whose operations fail when
|
|
733
803
|
* it is used, rather than throwing here.
|
|
@@ -738,6 +808,26 @@ export interface TableResult<T = any> extends AsyncIterable<{ row: SmartRow<T>;
|
|
|
738
808
|
index: number
|
|
739
809
|
) => SmartRow<T>;
|
|
740
810
|
|
|
811
|
+
/**
|
|
812
|
+
* ASYNC: Returns the row with a specific logical/data-model index, scrolling/paginating to
|
|
813
|
+
* reach it. Unlike the sync \`getRowByIndex\` (render-window position), this resolves the true
|
|
814
|
+
* data-model row \`index\` on virtualized tables.
|
|
815
|
+
*
|
|
816
|
+
* Reaches the row via, in order: a currently-mounted match, the viewport's random-access
|
|
817
|
+
* \`scrollToRow\` fast path, then advancing pages (on infinite-scroll tables a "page" is a
|
|
818
|
+
* scroll step) up to \`maxPages\`.
|
|
819
|
+
*
|
|
820
|
+
* Requires a \`strategies.resolveRowIndex\` to identify rows by logical index — throws if one
|
|
821
|
+
* is not configured. Also throws if the row cannot be reached (no silent wrong-row fallback).
|
|
822
|
+
*
|
|
823
|
+
* @param index 0-based logical/data-model row index
|
|
824
|
+
* @param options - \`maxPages\` bounds how far to scroll/paginate (defaults to config.maxPages)
|
|
825
|
+
*/
|
|
826
|
+
findRowByIndex: (
|
|
827
|
+
index: number,
|
|
828
|
+
options?: { maxPages?: number }
|
|
829
|
+
) => Promise<SmartRow<T>>;
|
|
830
|
+
|
|
741
831
|
/**
|
|
742
832
|
* ASYNC: Searches for a single row across pages using pagination.
|
|
743
833
|
* Auto-initializes the table if not already initialized.
|
package/dist/types.d.ts
CHANGED
|
@@ -157,6 +157,15 @@ export interface ViewportStrategy {
|
|
|
157
157
|
first: number;
|
|
158
158
|
last: number;
|
|
159
159
|
}>;
|
|
160
|
+
/**
|
|
161
|
+
* Returns the DOM positions (0-based, document order — indices into the resolved
|
|
162
|
+
* `rowSelector` set) of rows currently within the scroll container's visible bounds.
|
|
163
|
+
* Unlike `getVisibleRowRange` (a logical min/max), this identifies the exact rows, so
|
|
164
|
+
* `map`/`forEach`/`filter` can skip overscan rows during collection (see #353 / #357).
|
|
165
|
+
* Geometry-based and inclusive (any overlap counts as visible). When the container can't
|
|
166
|
+
* be measured, return all positions so no rows are filtered.
|
|
167
|
+
*/
|
|
168
|
+
getVisibleRowIndices?: (context: TableContext) => Promise<number[]>;
|
|
160
169
|
/**
|
|
161
170
|
* Returns the 0-based index range of columns currently rendered in the DOM.
|
|
162
171
|
* Used to detect when a target column is not yet mounted before reading.
|
|
@@ -236,9 +245,16 @@ export type SmartRow<T = any> = Locator & {
|
|
|
236
245
|
*
|
|
237
246
|
* const partial = await row.toJSON({ columns: ['Name', 'Email'] });
|
|
238
247
|
* // { Name: 'John', Email: 'john@example.com' }
|
|
248
|
+
*
|
|
249
|
+
* // Atomic: snapshot all cell values in a single evaluate — zero inter-column stagger.
|
|
250
|
+
* // Requires cellSelector to be a CSS string (not a function).
|
|
251
|
+
* // Column overrides ARE supported (run against a frozen off-screen reconstruction).
|
|
252
|
+
* // Uses textContent (not layout-dependent innerText) for non-override columns.
|
|
253
|
+
* const coherent = await row.toJSON({ atomic: true });
|
|
239
254
|
*/
|
|
240
255
|
toJSON(options?: {
|
|
241
256
|
columns?: string[];
|
|
257
|
+
atomic?: boolean;
|
|
242
258
|
}): Promise<T>;
|
|
243
259
|
/**
|
|
244
260
|
* Scrolls/paginates to bring this row into view.
|
|
@@ -383,12 +399,42 @@ export type FillStrategy = (options: {
|
|
|
383
399
|
table: TableResult;
|
|
384
400
|
fillOptions?: FillOptions;
|
|
385
401
|
}) => Promise<void>;
|
|
402
|
+
/** Context passed as the second argument to {@link ColumnOverride.read}. */
|
|
403
|
+
export interface ColumnOverrideReadContext {
|
|
404
|
+
/**
|
|
405
|
+
* The parent row. Use for multi-cell or row-derived values — e.g. a synthetic column
|
|
406
|
+
* that reads an `a[href]` or a `data-*` attribute from the row rather than a cell:
|
|
407
|
+
* `read: (_cell, { row }) => row.evaluate(el => el.querySelector('a')?.href)`.
|
|
408
|
+
*/
|
|
409
|
+
row: SmartRow;
|
|
410
|
+
/** The column being read. */
|
|
411
|
+
columnName: string;
|
|
412
|
+
/** The column's 0-based index in the resolved header map. */
|
|
413
|
+
columnIndex: number;
|
|
414
|
+
/**
|
|
415
|
+
* Get a Locator for another cell in the same row by column name.
|
|
416
|
+
* Returns raw cell Locators, not override-processed values.
|
|
417
|
+
*
|
|
418
|
+
* In atomic mode, the Locator points at the frozen reconstructed cell — coherent with
|
|
419
|
+
* every other cell from the same snapshot. In non-atomic mode, it points at the live cell.
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* read: async (_cell, { getCell }) => {
|
|
423
|
+
* const name = (await getCell('Name').innerText()).trim();
|
|
424
|
+
* const href = await getCell('Name').locator('a').getAttribute('href') || '';
|
|
425
|
+
* return `${name} | ${href}`;
|
|
426
|
+
* }
|
|
427
|
+
*/
|
|
428
|
+
getCell: (columnName: string) => Locator;
|
|
429
|
+
}
|
|
386
430
|
export interface ColumnOverride<TValue = any> {
|
|
387
431
|
/**
|
|
388
432
|
* How to extract the value from the cell.
|
|
389
|
-
* `context` provides
|
|
433
|
+
* The second `context` argument provides the parent `row` (for multi-cell or row-derived
|
|
434
|
+
* values), plus `columnName` and `columnIndex`. Backwards-compatible: existing
|
|
435
|
+
* single-argument `read(cell)` implementations keep working.
|
|
390
436
|
*/
|
|
391
|
-
read?: (cell: Locator) => Promise<TValue> | TValue;
|
|
437
|
+
read?: (cell: Locator, context: ColumnOverrideReadContext) => Promise<TValue> | TValue;
|
|
392
438
|
/**
|
|
393
439
|
* How to fill the cell with a new value. (Replaces smartFill default logic)
|
|
394
440
|
* Provides the current value (via `read`) if a `write` wants to check state first.
|
|
@@ -567,6 +613,12 @@ export interface TableConfig<T = any> {
|
|
|
567
613
|
* Overrides both default extraction (toJSON) and filling (smartFill) logic.
|
|
568
614
|
*/
|
|
569
615
|
columnOverrides?: Partial<Record<keyof T, ColumnOverride<T[keyof T]>>>;
|
|
616
|
+
/**
|
|
617
|
+
* Locator for an empty-state element that replaces the table when there are no results.
|
|
618
|
+
* If header resolution fails during init() and this locator is visible, init() succeeds
|
|
619
|
+
* and isEmpty() returns true. All row operations still throw normally.
|
|
620
|
+
*/
|
|
621
|
+
emptyState?: Locator;
|
|
570
622
|
}
|
|
571
623
|
export interface FinalTableConfig<T = any> extends TableConfig<T> {
|
|
572
624
|
headerSelector: string | ((root: Locator) => Locator);
|
|
@@ -595,9 +647,17 @@ export interface FillOptions {
|
|
|
595
647
|
/** Callback context passed to forEach, map, and filter. */
|
|
596
648
|
export type RowIterationContext<T = any> = {
|
|
597
649
|
row: SmartRow<T>;
|
|
598
|
-
/**
|
|
650
|
+
/**
|
|
651
|
+
* The row's logical/data-model index. When a `resolveRowIndex` strategy is configured
|
|
652
|
+
* (e.g. MUI DataGrid's `data-rowindex`) this is the grid's true row index; otherwise it
|
|
653
|
+
* equals `index`. Use this for `row.bringIntoView()` and position math on virtualized
|
|
654
|
+
* tables — it is stable across scrolling/dedupe, unlike the visit-order `index`.
|
|
655
|
+
*/
|
|
599
656
|
rowIndex: number;
|
|
600
|
-
/**
|
|
657
|
+
/**
|
|
658
|
+
* 0-based enumeration counter — the order this row was visited (contiguous within the run).
|
|
659
|
+
* Not a DOM position or grid identity. Use `rowIndex` for the row's data-model index.
|
|
660
|
+
*/
|
|
601
661
|
index: number;
|
|
602
662
|
/** 0-based page index — which page this row was collected from. */
|
|
603
663
|
pageIndex: number;
|
|
@@ -651,6 +711,12 @@ export interface TableResult<T = any> extends AsyncIterable<{
|
|
|
651
711
|
* @returns true if init() has been called and completed, false otherwise
|
|
652
712
|
*/
|
|
653
713
|
isInitialized(): boolean;
|
|
714
|
+
/**
|
|
715
|
+
* SYNC: Returns true if init() resolved via the emptyState path — the table's
|
|
716
|
+
* empty-state locator was visible when header resolution failed.
|
|
717
|
+
* Row operations still throw normally; use this to branch before calling them.
|
|
718
|
+
*/
|
|
719
|
+
isEmpty(): boolean;
|
|
654
720
|
getHeaders: () => Promise<string[]>;
|
|
655
721
|
getHeaderCell: (columnName: string) => Promise<Locator>;
|
|
656
722
|
/**
|
|
@@ -670,8 +736,9 @@ export interface TableResult<T = any> extends AsyncIterable<{
|
|
|
670
736
|
* For button-paginated tables this is the i-th row on the current page. For virtualized
|
|
671
737
|
* tables the render window shifts as you scroll, so `getRowByIndex(i)` returns whatever
|
|
672
738
|
* row currently sits at DOM position `i` — NOT the logical/absolute row `i` in the dataset
|
|
673
|
-
* once the list has scrolled.
|
|
674
|
-
* `
|
|
739
|
+
* once the list has scrolled. For the row with a specific logical/data-model index on a
|
|
740
|
+
* virtualized table, use the async `findRowByIndex(i)` instead (it scrolls to the row);
|
|
741
|
+
* to iterate by logical identity, use `map` / `findRows` (with a `dedupe` strategy).
|
|
675
742
|
*
|
|
676
743
|
* Resolution is lazy: an out-of-range index yields a SmartRow whose operations fail when
|
|
677
744
|
* it is used, rather than throwing here.
|
|
@@ -679,6 +746,24 @@ export interface TableResult<T = any> extends AsyncIterable<{
|
|
|
679
746
|
* @param index 0-based position within the current render window
|
|
680
747
|
*/
|
|
681
748
|
getRowByIndex: (index: number) => SmartRow<T>;
|
|
749
|
+
/**
|
|
750
|
+
* ASYNC: Returns the row with a specific logical/data-model index, scrolling/paginating to
|
|
751
|
+
* reach it. Unlike the sync `getRowByIndex` (render-window position), this resolves the true
|
|
752
|
+
* data-model row `index` on virtualized tables.
|
|
753
|
+
*
|
|
754
|
+
* Reaches the row via, in order: a currently-mounted match, the viewport's random-access
|
|
755
|
+
* `scrollToRow` fast path, then advancing pages (on infinite-scroll tables a "page" is a
|
|
756
|
+
* scroll step) up to `maxPages`.
|
|
757
|
+
*
|
|
758
|
+
* Requires a `strategies.resolveRowIndex` to identify rows by logical index — throws if one
|
|
759
|
+
* is not configured. Also throws if the row cannot be reached (no silent wrong-row fallback).
|
|
760
|
+
*
|
|
761
|
+
* @param index 0-based logical/data-model row index
|
|
762
|
+
* @param options - `maxPages` bounds how far to scroll/paginate (defaults to config.maxPages)
|
|
763
|
+
*/
|
|
764
|
+
findRowByIndex: (index: number, options?: {
|
|
765
|
+
maxPages?: number;
|
|
766
|
+
}) => Promise<SmartRow<T>>;
|
|
682
767
|
/**
|
|
683
768
|
* ASYNC: Searches for a single row across pages using pagination.
|
|
684
769
|
* Auto-initializes the table if not already initialized.
|
package/dist/useTable.js
CHANGED
|
@@ -27,6 +27,7 @@ const filterEngine_1 = require("./filterEngine");
|
|
|
27
27
|
const tableMapper_1 = require("./engine/tableMapper");
|
|
28
28
|
const rowFinder_1 = require("./engine/rowFinder");
|
|
29
29
|
const tableIteration_1 = require("./engine/tableIteration");
|
|
30
|
+
const rowResolution_1 = require("./engine/rowResolution");
|
|
30
31
|
const debugUtils_1 = require("./utils/debugUtils");
|
|
31
32
|
const smartRowArray_1 = require("./utils/smartRowArray");
|
|
32
33
|
const elementTracker_1 = require("./utils/elementTracker");
|
|
@@ -69,9 +70,11 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
69
70
|
const vp = config.strategies.viewport;
|
|
70
71
|
let colRangeCache = null;
|
|
71
72
|
let rowRangeCache = null;
|
|
73
|
+
let rowIndicesCache = null;
|
|
72
74
|
_clearViewportCache = () => {
|
|
73
75
|
colRangeCache = null;
|
|
74
76
|
rowRangeCache = null;
|
|
77
|
+
rowIndicesCache = null;
|
|
75
78
|
};
|
|
76
79
|
config.strategies.viewport = Object.assign(Object.assign({}, vp), { getVisibleColumnRange: vp.getVisibleColumnRange
|
|
77
80
|
? async (ctx) => {
|
|
@@ -85,15 +88,23 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
85
88
|
rowRangeCache = await vp.getVisibleRowRange(ctx);
|
|
86
89
|
return rowRangeCache;
|
|
87
90
|
}
|
|
91
|
+
: undefined, getVisibleRowIndices: vp.getVisibleRowIndices
|
|
92
|
+
? async (ctx) => {
|
|
93
|
+
if (!rowIndicesCache)
|
|
94
|
+
rowIndicesCache = await vp.getVisibleRowIndices(ctx);
|
|
95
|
+
return rowIndicesCache;
|
|
96
|
+
}
|
|
88
97
|
: undefined, scrollToColumn: vp.scrollToColumn
|
|
89
98
|
? async (ctx, colIndex) => {
|
|
90
99
|
colRangeCache = null;
|
|
91
100
|
rowRangeCache = null;
|
|
101
|
+
rowIndicesCache = null;
|
|
92
102
|
await vp.scrollToColumn(ctx, colIndex);
|
|
93
103
|
}
|
|
94
104
|
: undefined, scrollToRow: vp.scrollToRow
|
|
95
105
|
? async (ctx, rowIndex) => {
|
|
96
106
|
rowRangeCache = null;
|
|
107
|
+
rowIndicesCache = null;
|
|
97
108
|
await vp.scrollToRow(ctx, rowIndex);
|
|
98
109
|
}
|
|
99
110
|
: undefined });
|
|
@@ -137,7 +148,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
137
148
|
const _makeSmart = (rowLocator, map, rowIndex, tablePageIndex, barrier) => {
|
|
138
149
|
return (0, smartRow_1.default)(rowLocator, map, rowIndex, config, rootLocator, resolve, finalTable, tablePageIndex, barrier);
|
|
139
150
|
};
|
|
140
|
-
const tableState = { currentPageIndex: 0 };
|
|
151
|
+
const tableState = { currentPageIndex: 0, empty: false };
|
|
141
152
|
const rowFinder = new rowFinder_1.RowFinder(rootLocator, config, resolve, filterEngine, tableMapper, _makeSmart, tableState, (useBulk) => _advancePage(useBulk));
|
|
142
153
|
/** Builds a full TableContext/StrategyContext with getHeaderCell, getHeaders, scrollToColumn. Set after result is created. */
|
|
143
154
|
let createStrategyContext = () => ({ root: rootLocator, config, page: rootLocator.page(), resolve });
|
|
@@ -215,7 +226,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
215
226
|
set currentPageIndex(v) { tableState.currentPageIndex = v; },
|
|
216
227
|
init: async (options) => {
|
|
217
228
|
var _a;
|
|
218
|
-
if (tableMapper.isInitialized())
|
|
229
|
+
if (tableMapper.isInitialized() || tableState.empty)
|
|
219
230
|
return result;
|
|
220
231
|
if (config.strategies.sorting)
|
|
221
232
|
(0, validation_1.validateSortingStrategy)(config.strategies.sorting);
|
|
@@ -223,7 +234,21 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
223
234
|
(0, validation_1.validateFillStrategy)(config.strategies.fill);
|
|
224
235
|
(0, debugUtils_1.warnIfDebugInCI)(config);
|
|
225
236
|
(0, debugUtils_1.logDebug)(config, 'info', 'Initializing table');
|
|
226
|
-
|
|
237
|
+
let map;
|
|
238
|
+
try {
|
|
239
|
+
map = await tableMapper.getMap(options === null || options === void 0 ? void 0 : options.timeout);
|
|
240
|
+
}
|
|
241
|
+
catch (headerError) {
|
|
242
|
+
if (config.emptyState) {
|
|
243
|
+
const visible = await config.emptyState.isVisible().catch(() => false);
|
|
244
|
+
if (visible) {
|
|
245
|
+
tableState.empty = true;
|
|
246
|
+
(0, debugUtils_1.logDebug)(config, 'info', 'init: header resolution failed but emptyState locator is visible — table is empty');
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
throw headerError;
|
|
251
|
+
}
|
|
227
252
|
(0, debugUtils_1.logDebug)(config, 'info', `Table initialized with ${map.size} columns`, Array.from(map.keys()));
|
|
228
253
|
if ((_a = config.strategies.pagination) === null || _a === void 0 ? void 0 : _a.detectCurrentPage) {
|
|
229
254
|
try {
|
|
@@ -269,6 +294,8 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
269
294
|
return resolve(config.headerSelector, rootLocator).nth(idx);
|
|
270
295
|
},
|
|
271
296
|
countRows: async () => {
|
|
297
|
+
if (tableState.empty)
|
|
298
|
+
return 0;
|
|
272
299
|
await _autoInit();
|
|
273
300
|
const pag = config.strategies.pagination;
|
|
274
301
|
const hasPagination = config.maxPages > 1 && !!((pag === null || pag === void 0 ? void 0 : pag.goNext) || (pag === null || pag === void 0 ? void 0 : pag.goNextBulk));
|
|
@@ -326,7 +353,10 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
326
353
|
await cell.bringIntoView();
|
|
327
354
|
const columnOverride = (_a = config.columnOverrides) === null || _a === void 0 ? void 0 : _a[columnName];
|
|
328
355
|
if (columnOverride === null || columnOverride === void 0 ? void 0 : columnOverride.read) {
|
|
329
|
-
return await columnOverride.read(cell
|
|
356
|
+
return await columnOverride.read(cell, {
|
|
357
|
+
row, columnName, columnIndex: map.get(columnName),
|
|
358
|
+
getCell: (name) => row.getCell(name),
|
|
359
|
+
});
|
|
330
360
|
}
|
|
331
361
|
const text = await cell.innerText();
|
|
332
362
|
return (text || '').trim();
|
|
@@ -349,6 +379,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
349
379
|
log("No goToFirst strategy configured. Table may not be on page 1.");
|
|
350
380
|
}
|
|
351
381
|
tableState.currentPageIndex = 0;
|
|
382
|
+
tableState.empty = false;
|
|
352
383
|
tableMapper.clear();
|
|
353
384
|
log("Table reset complete. Calling autoInit to restore state.");
|
|
354
385
|
await _autoInit();
|
|
@@ -356,6 +387,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
356
387
|
revalidate: async () => {
|
|
357
388
|
log("Revalidating table structure...");
|
|
358
389
|
await tableMapper.remapHeaders();
|
|
390
|
+
tableState.empty = false;
|
|
359
391
|
log("Table revalidated.");
|
|
360
392
|
},
|
|
361
393
|
getRow: (filters, options = { exact: false }) => {
|
|
@@ -375,6 +407,55 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
375
407
|
const rowLocator = resolve(config.rowSelector, rootLocator).nth(index);
|
|
376
408
|
return _makeSmart(rowLocator, map, index);
|
|
377
409
|
},
|
|
410
|
+
findRowByIndex: async (index, options) => {
|
|
411
|
+
var _a, _b;
|
|
412
|
+
log(`findRowByIndex: index=${index} options=${safeStringify(options)}`);
|
|
413
|
+
await _autoInit();
|
|
414
|
+
const map = tableMapper.getMapSync();
|
|
415
|
+
const resolveRI = config.strategies.resolveRowIndex;
|
|
416
|
+
const viewport = config.strategies.viewport;
|
|
417
|
+
// Identifying a row by its logical index requires a resolveRowIndex strategy. Without one
|
|
418
|
+
// the library can only offer render-window-relative getRowByIndex, or filter-based findRow.
|
|
419
|
+
if (!resolveRI) {
|
|
420
|
+
throw new Error(`findRowByIndex(${index}) requires a strategies.resolveRowIndex to identify rows by their logical index. ` +
|
|
421
|
+
`Configure one (e.g. read the grid's row-index attribute), or use getRowByIndex() (current render window) / findRow(filters).`);
|
|
422
|
+
}
|
|
423
|
+
// The currently-mounted row whose logical index === index, if any.
|
|
424
|
+
const findMounted = async () => {
|
|
425
|
+
const rows = await resolve(config.rowSelector, rootLocator).all();
|
|
426
|
+
for (const r of rows) {
|
|
427
|
+
if ((await resolveRI(r)) === index)
|
|
428
|
+
return r;
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
};
|
|
432
|
+
// 1. Already mounted (no navigation needed)?
|
|
433
|
+
let loc = await findMounted();
|
|
434
|
+
// 2. Fast path: jump straight to it via the viewport's random-access scroll.
|
|
435
|
+
if (!loc && (viewport === null || viewport === void 0 ? void 0 : viewport.scrollToRow)) {
|
|
436
|
+
log(`findRowByIndex: scrolling to row ${index}`);
|
|
437
|
+
await viewport.scrollToRow(createStrategyContext(), index);
|
|
438
|
+
loc = await findMounted();
|
|
439
|
+
}
|
|
440
|
+
// 3. Fallback: advance pages (on infinite-scroll tables, a "page" is a scroll step) until
|
|
441
|
+
// the row is reached. Bounded by maxPages so an unreachable index can't loop forever.
|
|
442
|
+
if (!loc && config.strategies.pagination) {
|
|
443
|
+
const effectiveMaxPages = (_a = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
|
|
444
|
+
let pagesScanned = 1;
|
|
445
|
+
while (!loc && pagesScanned < effectiveMaxPages) {
|
|
446
|
+
if (!await _advancePage(false))
|
|
447
|
+
break;
|
|
448
|
+
pagesScanned++;
|
|
449
|
+
loc = await findMounted();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (!loc) {
|
|
453
|
+
throw new Error(`findRowByIndex(${index}): could not reach row ${index}. ` +
|
|
454
|
+
`Ensure a viewport.scrollToRow or pagination can bring it into view (searched up to maxPages=${(_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages}).`);
|
|
455
|
+
}
|
|
456
|
+
await (0, debugUtils_1.debugDelay)(config, 'findRow');
|
|
457
|
+
return _makeSmart(loc, map, index, tableState.currentPageIndex);
|
|
458
|
+
},
|
|
378
459
|
findRow: async (filters, options) => {
|
|
379
460
|
log(`findRow: filters=${safeStringify(filters)} options=${safeStringify(options)}`);
|
|
380
461
|
return rowFinder.findRow(filters, options);
|
|
@@ -384,10 +465,13 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
384
465
|
return rowFinder.findRows(filters !== null && filters !== void 0 ? filters : {}, options);
|
|
385
466
|
},
|
|
386
467
|
isInitialized: () => {
|
|
387
|
-
const initialized = tableMapper.isInitialized();
|
|
468
|
+
const initialized = tableMapper.isInitialized() || tableState.empty;
|
|
388
469
|
log(`isInitialized: ${initialized}`);
|
|
389
470
|
return initialized;
|
|
390
471
|
},
|
|
472
|
+
isEmpty: () => {
|
|
473
|
+
return tableState.empty;
|
|
474
|
+
},
|
|
391
475
|
sorting: {
|
|
392
476
|
apply: async (columnName, direction) => {
|
|
393
477
|
var _a;
|
|
@@ -434,6 +518,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
434
518
|
// ─── Shared async row iterator ───────────────────────────────────────────
|
|
435
519
|
[Symbol.asyncIterator]() {
|
|
436
520
|
return __asyncGenerator(this, arguments, function* _a() {
|
|
521
|
+
var _b;
|
|
437
522
|
yield __await(_autoInit());
|
|
438
523
|
const map = tableMapper.getMapSync();
|
|
439
524
|
const effectiveMaxPages = config.maxPages;
|
|
@@ -449,7 +534,13 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
449
534
|
const pageRows = yield __await(rowLocators.all());
|
|
450
535
|
const barrier = new navigationBarrier_1.NavigationBarrier(newIndices.length);
|
|
451
536
|
for (const idx of newIndices) {
|
|
452
|
-
|
|
537
|
+
// index = visit-order counter; rowIndex = logical/data index (B-hybrid, #362).
|
|
538
|
+
const logical = (_b = yield __await((0, rowResolution_1.resolveLogicalRowIndex)(pageRows[idx], config, () => rowIndex))) !== null && _b !== void 0 ? _b : rowIndex;
|
|
539
|
+
const sr = _makeSmart(pageRows[idx], map, logical, tableState.currentPageIndex, barrier);
|
|
540
|
+
// Iterator rows share one per-page DOM snapshot (positional locators); disable
|
|
541
|
+
// toJSON's scroll-back recovery so reading one row can't invalidate the others (#366).
|
|
542
|
+
sr._inBatch = true;
|
|
543
|
+
yield yield __await({ row: sr, index: rowIndex, rowIndex: logical, pageIndex: tableState.currentPageIndex });
|
|
453
544
|
rowIndex++;
|
|
454
545
|
}
|
|
455
546
|
if (pagesScanned >= effectiveMaxPages)
|
|
@@ -477,6 +568,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
477
568
|
config,
|
|
478
569
|
getPage: () => rootLocator.page(),
|
|
479
570
|
getCurrentPageIndex: () => tableState.currentPageIndex,
|
|
571
|
+
getContext: () => createStrategyContext(),
|
|
480
572
|
}, callback, options);
|
|
481
573
|
},
|
|
482
574
|
map: async (callback, options = {}) => {
|
|
@@ -491,6 +583,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
491
583
|
config,
|
|
492
584
|
getPage: () => rootLocator.page(),
|
|
493
585
|
getCurrentPageIndex: () => tableState.currentPageIndex,
|
|
586
|
+
getContext: () => createStrategyContext(),
|
|
494
587
|
}, callback, options);
|
|
495
588
|
},
|
|
496
589
|
toArray: async (callback, options = {}) => {
|
|
@@ -515,6 +608,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
515
608
|
config,
|
|
516
609
|
getPage: () => rootLocator.page(),
|
|
517
610
|
getCurrentPageIndex: () => tableState.currentPageIndex,
|
|
611
|
+
getContext: () => createStrategyContext(),
|
|
518
612
|
}, predicate, options);
|
|
519
613
|
},
|
|
520
614
|
generateConfig: async () => {
|