@rickcedwhat/playwright-smart-table 6.11.0 → 6.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,7 @@ await table.forEach(async ({ row }) => {
45
45
  npm install @rickcedwhat/playwright-smart-table
46
46
  ```
47
47
 
48
+
48
49
  ## Quick Start
49
50
 
50
51
  ```typescript
@@ -11,10 +11,9 @@ export declare class RowFinder<T = any> {
11
11
  private makeSmartRow;
12
12
  private tableState;
13
13
  private resolve;
14
- constructor(rootLocator: Locator, config: FinalTableConfig, resolve: (item: Selector, parent: Locator | Page) => Locator, filterEngine: FilterEngine, tableMapper: TableMapper, makeSmartRow: (loc: Locator, map: Map<string, number>, index: number, tablePageIndex?: number) => SmartRow<T>, tableState?: {
14
+ constructor(rootLocator: Locator, config: FinalTableConfig, resolve: (item: Selector, parent: Locator | Page) => Locator, filterEngine: FilterEngine, tableMapper: TableMapper, makeSmartRow: (loc: Locator, map: Map<string, number>, index: number | undefined, tablePageIndex?: number) => SmartRow<T>, tableState?: {
15
15
  currentPageIndex: number;
16
16
  });
17
- private log;
18
17
  findRow(filters: Record<string, FilterValue>, options?: {
19
18
  exact?: boolean;
20
19
  maxPages?: number;
@@ -22,6 +21,8 @@ export declare class RowFinder<T = any> {
22
21
  findRows(filters?: Record<string, FilterValue>, options?: {
23
22
  exact?: boolean;
24
23
  maxPages?: number;
24
+ useBulkPagination?: boolean;
25
25
  }): Promise<SmartRowArray<T>>;
26
26
  private findRowLocator;
27
+ private resolveRowIndex;
27
28
  }
@@ -16,9 +16,6 @@ class RowFinder {
16
16
  this.tableState = tableState;
17
17
  this.resolve = resolve;
18
18
  }
19
- log(msg) {
20
- (0, debugUtils_1.logDebug)(this.config, 'verbose', msg);
21
- }
22
19
  async findRow(filters, options = {}) {
23
20
  (0, debugUtils_1.logDebug)(this.config, 'info', 'Searching for row', filters);
24
21
  await this.tableMapper.getMap();
@@ -26,7 +23,9 @@ class RowFinder {
26
23
  if (rowLocator) {
27
24
  (0, debugUtils_1.logDebug)(this.config, 'info', 'Row found');
28
25
  await (0, debugUtils_1.debugDelay)(this.config, 'findRow');
29
- return this.makeSmartRow(rowLocator, await this.tableMapper.getMap(), 0);
26
+ const map = await this.tableMapper.getMap();
27
+ const rowIndex = await this.resolveRowIndex(rowLocator);
28
+ return this.makeSmartRow(rowLocator, map, rowIndex, this.tableState.currentPageIndex);
30
29
  }
31
30
  (0, debugUtils_1.logDebug)(this.config, 'error', 'Row not found', filters);
32
31
  await (0, debugUtils_1.debugDelay)(this.config, 'findRow');
@@ -43,7 +42,7 @@ class RowFinder {
43
42
  const allRows = [];
44
43
  const effectiveMaxPages = (_b = (_a = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages) !== null && _b !== void 0 ? _b : Infinity;
45
44
  let pagesScanned = 1;
46
- this.log(`findRows: starting (maxPages=${effectiveMaxPages}, filters=${JSON.stringify(filtersRecord)})`);
45
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: starting (maxPages=${effectiveMaxPages}, filters=${JSON.stringify(filtersRecord)})`);
47
46
  const tracker = new elementTracker_1.ElementTracker('findRows');
48
47
  try {
49
48
  const collectMatches = async () => {
@@ -61,13 +60,13 @@ class RowFinder {
61
60
  for (const idx of newIndices) {
62
61
  const smartRow = this.makeSmartRow(currentRows[idx], map, allRows.length, this.tableState.currentPageIndex);
63
62
  if (isRowLoading && await isRowLoading(smartRow)) {
64
- this.log(`findRows: page ${this.tableState.currentPageIndex} — row skipped (isRowLoading=true)`);
63
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: page ${this.tableState.currentPageIndex} — row skipped (isRowLoading=true)`);
65
64
  continue;
66
65
  }
67
66
  allRows.push(smartRow);
68
67
  added++;
69
68
  }
70
- this.log(`findRows: page ${this.tableState.currentPageIndex} — ${added} new match(es) (total: ${allRows.length})`);
69
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: page ${this.tableState.currentPageIndex} — ${added} new match(es) (total: ${allRows.length})`);
71
70
  };
72
71
  // Scan first page
73
72
  await collectMatches();
@@ -80,40 +79,42 @@ class RowFinder {
80
79
  page: this.rootLocator.page()
81
80
  };
82
81
  let paginationResult;
83
- if ((_c = this.config.strategies.pagination) === null || _c === void 0 ? void 0 : _c.goNextBulk) {
82
+ const useBulk = (options === null || options === void 0 ? void 0 : options.useBulkPagination) !== false && !!((_c = this.config.strategies.pagination) === null || _c === void 0 ? void 0 : _c.goNextBulk);
83
+ if (useBulk) {
84
84
  paginationResult = await this.config.strategies.pagination.goNextBulk(context);
85
85
  }
86
86
  else if ((_d = this.config.strategies.pagination) === null || _d === void 0 ? void 0 : _d.goNext) {
87
87
  paginationResult = await this.config.strategies.pagination.goNext(context);
88
88
  }
89
89
  else {
90
- this.log(`findRows: no pagination primitive — stopping`);
90
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: no pagination primitive — stopping`);
91
91
  break;
92
92
  }
93
93
  const didPaginate = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
94
94
  if (!didPaginate) {
95
- this.log(`findRows: pagination returned false — end of data`);
95
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: pagination returned false — end of data`);
96
96
  break;
97
97
  }
98
98
  const pagesJumped = typeof paginationResult === 'number' ? paginationResult : 1;
99
99
  this.tableState.currentPageIndex += pagesJumped;
100
100
  pagesScanned += pagesJumped;
101
- this.log(`findRows: advanced ${pagesJumped} page(s), now at page ${this.tableState.currentPageIndex}`);
101
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: advanced ${pagesJumped} page(s), now at page ${this.tableState.currentPageIndex}`);
102
+ await (0, debugUtils_1.debugDelay)(this.config, 'pagination');
102
103
  await collectMatches();
103
104
  }
104
105
  }
105
106
  finally {
106
107
  await tracker.cleanup(this.rootLocator.page());
107
108
  }
108
- this.log(`findRows: done — ${allRows.length} row(s) collected across ${pagesScanned} page(s)`);
109
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: done — ${allRows.length} row(s) collected across ${pagesScanned} page(s)`);
109
110
  return (0, smartRowArray_1.createSmartRowArray)(allRows);
110
111
  }
111
112
  async findRowLocator(filters, options = {}) {
112
- var _a, _b, _c, _d;
113
+ var _a, _b;
113
114
  const map = await this.tableMapper.getMap();
114
115
  const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages;
115
116
  let pagesScanned = 1;
116
- this.log(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
117
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
117
118
  while (true) {
118
119
  // Check Loading
119
120
  if ((_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isTableLoading) {
@@ -124,7 +125,7 @@ class RowFinder {
124
125
  resolve: this.resolve
125
126
  });
126
127
  if (isLoading) {
127
- this.log('Table is loading... waiting');
128
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', 'Table is loading... waiting');
128
129
  await this.rootLocator.page().waitForTimeout(200);
129
130
  continue;
130
131
  }
@@ -132,7 +133,7 @@ class RowFinder {
132
133
  const allRows = this.resolve(this.config.rowSelector, this.rootLocator);
133
134
  const matchedRows = this.filterEngine.applyFilters(allRows, filters, map, options.exact || false, this.rootLocator.page(), this.rootLocator);
134
135
  const count = await matchedRows.count();
135
- this.log(`Page ${this.tableState.currentPageIndex}: Found ${count} matches.`);
136
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Found ${count} matches.`);
136
137
  if (count > 1) {
137
138
  const sampleData = [];
138
139
  try {
@@ -151,7 +152,7 @@ class RowFinder {
151
152
  if (count === 1)
152
153
  return matchedRows.first();
153
154
  if (pagesScanned < effectiveMaxPages) {
154
- this.log(`Page ${this.tableState.currentPageIndex}: Not found. Attempting pagination...`);
155
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Not found. Attempting pagination...`);
155
156
  const context = {
156
157
  root: this.rootLocator,
157
158
  config: this.config,
@@ -159,14 +160,16 @@ class RowFinder {
159
160
  page: this.rootLocator.page()
160
161
  };
161
162
  let paginationResult;
162
- if ((_c = this.config.strategies.pagination) === null || _c === void 0 ? void 0 : _c.goNextBulk) {
163
- paginationResult = await this.config.strategies.pagination.goNextBulk(context);
163
+ const pagination = this.config.strategies.pagination;
164
+ const useBulk = options.useBulkPagination !== false && !!(pagination === null || pagination === void 0 ? void 0 : pagination.goNextBulk);
165
+ if (useBulk && (pagination === null || pagination === void 0 ? void 0 : pagination.goNextBulk)) {
166
+ paginationResult = await pagination.goNextBulk(context);
164
167
  }
165
- else if ((_d = this.config.strategies.pagination) === null || _d === void 0 ? void 0 : _d.goNext) {
166
- paginationResult = await this.config.strategies.pagination.goNext(context);
168
+ else if (pagination === null || pagination === void 0 ? void 0 : pagination.goNext) {
169
+ paginationResult = await pagination.goNext(context);
167
170
  }
168
171
  else {
169
- this.log(`Page ${this.tableState.currentPageIndex}: Pagination failed (no goNext or goNextBulk primitive).`);
172
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Pagination failed (no goNext or goNextBulk primitive).`);
170
173
  return null;
171
174
  }
172
175
  const didLoadMore = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
@@ -174,14 +177,29 @@ class RowFinder {
174
177
  const pagesJumped = typeof paginationResult === 'number' ? paginationResult : 1;
175
178
  this.tableState.currentPageIndex += pagesJumped;
176
179
  pagesScanned += pagesJumped;
180
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRowLocator: advanced ${pagesJumped} page(s), now at page ${this.tableState.currentPageIndex}`);
181
+ await (0, debugUtils_1.debugDelay)(this.config, 'pagination');
177
182
  continue;
178
183
  }
179
184
  else {
180
- this.log(`Page ${this.tableState.currentPageIndex}: Pagination failed (end of data).`);
185
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Pagination failed (end of data).`);
181
186
  }
182
187
  }
183
188
  return null;
184
189
  }
185
190
  }
191
+ async resolveRowIndex(rowLocator) {
192
+ const allRows = await this.resolve(this.config.rowSelector, this.rootLocator).all();
193
+ const targetHandle = await rowLocator.elementHandle();
194
+ if (!targetHandle)
195
+ return undefined;
196
+ for (let i = 0; i < allRows.length; i++) {
197
+ const handle = await allRows[i].elementHandle();
198
+ if (handle && await handle.evaluate((el, t) => el === t, targetHandle)) {
199
+ return i;
200
+ }
201
+ }
202
+ return undefined;
203
+ }
186
204
  }
187
205
  exports.RowFinder = RowFinder;
@@ -11,6 +11,7 @@ export interface TableIterationEnv<T = any> {
11
11
  createSmartRowArray: (rows: SmartRow<T>[]) => SmartRowArray<T>;
12
12
  config: FinalTableConfig<T>;
13
13
  getPage: () => Page;
14
+ getCurrentPageIndex: () => number;
14
15
  }
15
16
  /** Delegates to {@link runMap}; void results are discarded. */
16
17
  export declare function runForEach<T>(env: TableIterationEnv<T>, callback: (ctx: RowIterationContext<T>) => void | Promise<void>, options?: RowIterationOptions): Promise<void>;
@@ -27,7 +27,7 @@ async function runMap(env, callback, options = {}, label = 'map') {
27
27
  const dedupeKeys = dedupeStrategy ? new Set() : null;
28
28
  const defaultMode = label === 'map' ? 'parallel' : 'sequential';
29
29
  const concurrency = (_d = (_c = options.concurrency) !== null && _c !== void 0 ? _c : env.config.concurrency) !== null && _d !== void 0 ? _d : defaultMode;
30
- const useBarrier = concurrency !== 'sequential';
30
+ const useBarrier = concurrency === 'synchronized';
31
31
  // Mutex must not pair with the navigation barrier: synchronized mode needs every row
32
32
  // to enter barrier.sync concurrently; serializing callbacks here deadlocks (first row waits
33
33
  // for batchSize peers that never reach the barrier).
@@ -50,8 +50,15 @@ async function runMap(env, callback, options = {}, label = 'map') {
50
50
  };
51
51
  while (!stopped) {
52
52
  const rowLocators = env.getRowLocators();
53
- const newIndices = await tracker.getUnseenIndices(rowLocators);
53
+ const allIndices = await tracker.peekUnseenIndices(rowLocators);
54
54
  const pageRows = await rowLocators.all();
55
+ // In synchronized mode, overscan rows rendered beyond the visible viewport by virtual
56
+ // scrollers can be evicted when the horizontal barrier fires snapFirstColumnIntoView.
57
+ // Filter them out before committing so they stay unseen and are picked up on the next page.
58
+ const newIndices = concurrency === 'synchronized'
59
+ ? (await Promise.all(allIndices.map(async (idx) => ({ idx, present: pageRows[idx] != null && (await pageRows[idx].count()) > 0 })))).filter(r => r.present).map(r => r.idx)
60
+ : allIndices;
61
+ await tracker.commitIndices(rowLocators, newIndices);
55
62
  const batchSize = newIndices.length;
56
63
  if (batchSize === 0) {
57
64
  log(env.config, `${label}: page ${pagesScanned} — no new row(s) found`);
@@ -60,7 +67,7 @@ async function runMap(env, callback, options = {}, label = 'map') {
60
67
  log(env.config, `${label}: scanning page ${pagesScanned} — ${batchSize} new row(s)`);
61
68
  const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(batchSize) : undefined;
62
69
  const actionMutex = useMutex ? new mutex_1.Mutex() : null;
63
- const smartRows = newIndices.map((idx, i) => env.makeSmartRow(pageRows[idx], map, rowIndex + i, pagesScanned - 1, barrier));
70
+ const smartRows = newIndices.map((idx, i) => env.makeSmartRow(pageRows[idx], map, rowIndex + i, env.getCurrentPageIndex(), barrier));
64
71
  const processRow = async (row) => {
65
72
  try {
66
73
  if (stopped && row.rowIndex > stoppedIndex) {
@@ -79,7 +86,7 @@ async function runMap(env, callback, options = {}, label = 'map') {
79
86
  if (stopped && row.rowIndex > stoppedIndex)
80
87
  return SKIP;
81
88
  log(env.config, `${label}: processing row ${row.rowIndex}`);
82
- return await callback({ row, index: row.rowIndex, rowIndex: row.rowIndex, stop: () => stop(row.rowIndex) });
89
+ return await callback({ row, index: row.rowIndex, rowIndex: row.rowIndex, pageIndex: env.getCurrentPageIndex(), stop: () => stop(row.rowIndex) });
83
90
  };
84
91
  if (actionMutex) {
85
92
  return await actionMutex.run(runCallback);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { PLAYWRIGHT_SMART_TABLE_VERSION } from './packageVersion';
2
2
  export { useTable } from './useTable';
3
- export type { TableConfig, TableResult, SmartRow, Selector, FilterValue, PaginationPrimitives, SortingStrategy, FillOptions, RowIterationContext, RowIterationOptions, TableContext, StrategyContext, BeforeCellReadFn, GetCellLocatorFn, GetActiveCellFn, DebugConfig, } from './types';
3
+ export type { TableConfig, TableResult, SmartRow, SmartCell, Selector, FilterValue, PaginationPrimitives, SortingStrategy, FillOptions, RowIterationContext, RowIterationOptions, TableContext, StrategyContext, BeforeCellReadFn, GetCellLocatorFn, GetActiveCellFn, DebugConfig, } from './types';
4
4
  export { Strategies } from './strategies';
5
5
  export * as presets from './presets';
6
6
  /** @deprecated Use `presets` instead. Plugins will be removed in v7.0.0. */
@@ -3,4 +3,4 @@
3
3
  * Generated by scripts/embed-version.mjs from package.json
4
4
  */
5
5
  /** Semver of this package. Use in logs or diagnostics to confirm the installed build. */
6
- export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.11.0";
6
+ export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.13.0";
@@ -6,4 +6,4 @@ exports.PLAYWRIGHT_SMART_TABLE_VERSION = void 0;
6
6
  * Generated by scripts/embed-version.mjs from package.json
7
7
  */
8
8
  /** Semver of this package. Use in logs or diagnostics to confirm the installed build. */
9
- exports.PLAYWRIGHT_SMART_TABLE_VERSION = "6.11.0";
9
+ exports.PLAYWRIGHT_SMART_TABLE_VERSION = "6.13.0";
@@ -8,15 +8,15 @@ export declare const Plugins: {
8
8
  /** @deprecated Use `presets.rdg` */
9
9
  RDG: {
10
10
  Strategies: import("../types").TableStrategies | undefined;
11
- headerSelector?: string | ((root: import("playwright-core").Locator) => import("playwright-core").Locator) | undefined;
11
+ headerSelector?: string | ((root: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
12
12
  rowSelector?: string | undefined;
13
- cellSelector?: string | ((row: import("playwright-core").Locator) => import("playwright-core").Locator) | undefined;
13
+ cellSelector?: string | ((row: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
14
14
  maxPages?: number | undefined;
15
15
  concurrency?: import("../types").RowIterationMode | undefined;
16
16
  headerTransformer?: ((args: {
17
17
  text: string;
18
18
  index: number;
19
- locator: import("playwright-core").Locator;
19
+ locator: import("@playwright/test").Locator;
20
20
  seenHeaders: Set<string>;
21
21
  }) => string | Promise<string>) | undefined;
22
22
  autoScroll?: boolean | undefined;
@@ -28,15 +28,15 @@ export declare const Plugins: {
28
28
  /** @deprecated Use `presets.glide` */
29
29
  Glide: {
30
30
  Strategies: import("../types").TableStrategies | undefined;
31
- headerSelector?: string | ((root: import("playwright-core").Locator) => import("playwright-core").Locator) | undefined;
31
+ headerSelector?: string | ((root: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
32
32
  rowSelector?: string | undefined;
33
- cellSelector?: string | ((row: import("playwright-core").Locator) => import("playwright-core").Locator) | undefined;
33
+ cellSelector?: string | ((row: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
34
34
  maxPages?: number | undefined;
35
35
  concurrency?: import("../types").RowIterationMode | undefined;
36
36
  headerTransformer?: ((args: {
37
37
  text: string;
38
38
  index: number;
39
- locator: import("playwright-core").Locator;
39
+ locator: import("@playwright/test").Locator;
40
40
  seenHeaders: Set<string>;
41
41
  }) => string | Promise<string>) | undefined;
42
42
  autoScroll?: boolean | undefined;
@@ -48,15 +48,15 @@ export declare const Plugins: {
48
48
  /** @deprecated Use `presets.muiDataGrid` */
49
49
  MUI: {
50
50
  Strategies: import("../types").TableStrategies | undefined;
51
- headerSelector?: string | ((root: import("playwright-core").Locator) => import("playwright-core").Locator) | undefined;
51
+ headerSelector?: string | ((root: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
52
52
  rowSelector?: string | undefined;
53
- cellSelector?: string | ((row: import("playwright-core").Locator) => import("playwright-core").Locator) | undefined;
53
+ cellSelector?: string | ((row: import("@playwright/test").Locator) => import("@playwright/test").Locator) | undefined;
54
54
  maxPages?: number | undefined;
55
55
  concurrency?: import("../types").RowIterationMode | undefined;
56
56
  headerTransformer?: ((args: {
57
57
  text: string;
58
58
  index: number;
59
- locator: import("playwright-core").Locator;
59
+ locator: import("@playwright/test").Locator;
60
60
  seenHeaders: Set<string>;
61
61
  }) => string | Promise<string>) | undefined;
62
62
  autoScroll?: boolean | undefined;
@@ -1,4 +1,7 @@
1
1
  import { FillStrategy, TableConfig } from '../../types';
2
+ export { createGlideViewport } from './viewport';
3
+ export type { GlideViewportOptions } from './viewport';
4
+ import { GlideViewportOptions } from './viewport';
2
5
  /** Strategies only for Glide Data Grid. Includes fillSimple; use when you want to supply your own selectors or override fill. */
3
6
  export declare const GlideStrategies: {
4
7
  fillSimple: FillStrategy;
@@ -9,15 +12,7 @@ export declare const GlideStrategies: {
9
12
  selector?: string;
10
13
  scrollAmount?: number;
11
14
  }) => Promise<string[]>;
12
- navigation: {
13
- goUp: (context: import("../..").StrategyContext) => Promise<void>;
14
- goDown: (context: import("../..").StrategyContext) => Promise<void>;
15
- goLeft: (context: import("../..").StrategyContext) => Promise<void>;
16
- goRight: (context: import("../..").StrategyContext) => Promise<void>;
17
- goHome: (context: import("../..").StrategyContext) => Promise<void>;
18
- snapFirstColumnIntoView: (context: import("../..").StrategyContext) => Promise<void>;
19
- seekColumnIndex: (context: import("../..").StrategyContext, columnIndex: number) => Promise<void>;
20
- };
15
+ viewport: import("../../types").ViewportStrategy;
21
16
  loading: {
22
17
  isHeaderLoading: () => Promise<boolean>;
23
18
  };
@@ -28,6 +23,16 @@ export declare const GlideStrategies: {
28
23
  locator: any;
29
24
  } | null>;
30
25
  };
26
+ /** Alias of {@link GlideViewportOptions}. All fields are forwarded to the viewport strategy. */
27
+ export type GlideOptions = GlideViewportOptions;
28
+ /**
29
+ * Factory that returns a configured Glide preset.
30
+ * Use when your grid differs from the defaults:
31
+ * ```ts
32
+ * useTable(loc, { ...createGlide({ attachTimeout: 5000 }), maxPages: 5 })
33
+ * ```
34
+ */
35
+ export declare function createGlide(options?: GlideOptions): Partial<TableConfig>;
31
36
  export declare const Glide: Partial<TableConfig> & {
32
37
  Strategies: typeof GlideStrategies;
33
38
  };
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Glide = exports.GlideStrategies = void 0;
4
- const columns_1 = require("./columns");
3
+ exports.Glide = exports.GlideStrategies = exports.createGlideViewport = void 0;
4
+ exports.createGlide = createGlide;
5
5
  const headers_1 = require("./headers");
6
6
  const pagination_1 = require("../../strategies/pagination");
7
7
  const stabilization_1 = require("../../strategies/stabilization");
8
+ var viewport_1 = require("./viewport");
9
+ Object.defineProperty(exports, "createGlideViewport", { enumerable: true, get: function () { return viewport_1.createGlideViewport; } });
10
+ const viewport_2 = require("./viewport");
8
11
  const glideFillStrategy = async ({ row, columnName, value, page }) => {
9
12
  // Canvas-aware click: Glide is a canvas grid. The accessibility 'td' elements
10
13
  // may not trigger internal focus on a simple click. We click the canvas at the cell's location.
@@ -86,15 +89,7 @@ const GlideDefaultStrategies = {
86
89
  fill: glideFillStrategy,
87
90
  pagination: glidePaginationStrategy,
88
91
  header: headers_1.scrollRightHeader,
89
- navigation: {
90
- goUp: columns_1.glideGoUp,
91
- goDown: columns_1.glideGoDown,
92
- goLeft: columns_1.glideGoLeft,
93
- goRight: columns_1.glideGoRight,
94
- goHome: columns_1.glideSnapFirstColumnIntoView,
95
- snapFirstColumnIntoView: columns_1.glideSnapFirstColumnIntoView,
96
- seekColumnIndex: columns_1.glideSeekColumnIndex
97
- },
92
+ viewport: (0, viewport_2.createGlideViewport)(),
98
93
  loading: {
99
94
  isHeaderLoading: async () => false
100
95
  },
@@ -103,10 +98,27 @@ const GlideDefaultStrategies = {
103
98
  };
104
99
  /** Strategies only for Glide Data Grid. Includes fillSimple; use when you want to supply your own selectors or override fill. */
105
100
  exports.GlideStrategies = Object.assign(Object.assign({}, GlideDefaultStrategies), { fillSimple: glideFillSimple });
101
+ /**
102
+ * Factory that returns a configured Glide preset.
103
+ * Use when your grid differs from the defaults:
104
+ * ```ts
105
+ * useTable(loc, { ...createGlide({ attachTimeout: 5000 }), maxPages: 5 })
106
+ * ```
107
+ */
108
+ function createGlide(options = {}) {
109
+ return {
110
+ headerSelector: 'table[role="grid"] thead tr th',
111
+ rowSelector: 'table[role="grid"] tbody tr',
112
+ cellSelector: 'td',
113
+ concurrency: 'sequential',
114
+ strategies: Object.assign(Object.assign({}, GlideDefaultStrategies), { viewport: (0, viewport_2.createGlideViewport)(options) })
115
+ };
116
+ }
106
117
  /**
107
118
  * Full preset for Glide Data Grid (selectors + default strategies only).
108
119
  * Spread: useTable(loc, { ...Plugins.Glide, maxPages: 5 }).
109
120
  * Strategies only (including fillSimple): useTable(loc, { rowSelector: '...', strategies: Plugins.Glide.Strategies }).
121
+ * For a non-64-column grid, use `createGlide({ columnCount })` instead.
110
122
  */
111
123
  const GlidePreset = {
112
124
  headerSelector: 'table[role="grid"] thead tr th',
@@ -0,0 +1,9 @@
1
+ import { ViewportStrategy } from '../../types';
2
+ export interface GlideViewportOptions {
3
+ /**
4
+ * Milliseconds to wait for a cell/row to appear in the DOM after a scroll.
5
+ * Default: 3000.
6
+ */
7
+ attachTimeout?: number;
8
+ }
9
+ export declare const createGlideViewport: (options?: GlideViewportOptions) => ViewportStrategy;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGlideViewport = void 0;
4
+ /**
5
+ * Viewport strategy for Glide Data Grid.
6
+ *
7
+ * Glide renders an ARIA accessibility table (`table[role="grid"]`) alongside its
8
+ * canvas. Rows and cells are virtualized: only the currently visible window of
9
+ * `tbody tr` / `td[aria-colindex]` elements exists in the DOM at any moment.
10
+ *
11
+ * This strategy:
12
+ * - `getVisibleColumnRange` — reads `aria-colindex` values from the first visible row
13
+ * - `getVisibleRowRange` — reads `aria-rowindex` values from visible rows (2-based: header = 1)
14
+ * - `scrollToColumn` — sets `.dvn-scroller.scrollLeft` via ratio derived from header count, waits for cell mount
15
+ * - `scrollToRow` — sets `.dvn-scroller.scrollTop` via measured row height, waits for row mount
16
+ */
17
+ const sanitize = (value, fallback) => {
18
+ const n = Number(value);
19
+ return Number.isFinite(n) && n > 0 ? n : fallback;
20
+ };
21
+ const createGlideViewport = (options = {}) => {
22
+ const attachTimeout = sanitize(options.attachTimeout, 3000);
23
+ return {
24
+ getVisibleColumnRange: async ({ page }) => {
25
+ const indices = await page
26
+ .locator('table[role="grid"] tbody tr')
27
+ .first()
28
+ .locator('td[aria-colindex]')
29
+ .evaluateAll((tds) => tds.map((td) => parseInt(td.getAttribute('aria-colindex') || '1') - 1));
30
+ if (!indices.length)
31
+ return { first: 0, last: 0 };
32
+ return { first: Math.min(...indices), last: Math.max(...indices) };
33
+ },
34
+ getVisibleRowRange: async ({ page }) => {
35
+ // aria-rowindex is 1-based; header row = 1, so data row 0 → aria-rowindex="2"
36
+ const indices = await page
37
+ .locator('table[role="grid"] tbody tr[aria-rowindex]')
38
+ .evaluateAll((trs) => trs.map((tr) => parseInt(tr.getAttribute('aria-rowindex') || '2') - 2));
39
+ if (!indices.length)
40
+ return { first: 0, last: 0 };
41
+ return { first: Math.min(...indices), last: Math.max(...indices) };
42
+ },
43
+ scrollToColumn: async (context, colIndex) => {
44
+ var _a, _b;
45
+ const { page } = context;
46
+ const scroller = page.locator('.dvn-scroller').first();
47
+ // Derive total column count from the initialized header map (available post-init).
48
+ // Falls back to a ratio seek only by position if headers are unavailable.
49
+ const headers = await ((_a = context.getHeaders) === null || _a === void 0 ? void 0 : _a.call(context));
50
+ const totalColumns = (_b = headers === null || headers === void 0 ? void 0 : headers.length) !== null && _b !== void 0 ? _b : 0;
51
+ if (totalColumns > 0) {
52
+ await scroller.evaluate((el, { idx, count }) => {
53
+ const max = Math.max(0, el.scrollWidth - el.clientWidth);
54
+ const ratio = Math.min(1, Math.max(0, (idx + 0.5) / count));
55
+ el.scrollLeft = Math.floor(ratio * max);
56
+ }, { idx: colIndex, count: totalColumns });
57
+ }
58
+ const target = page
59
+ .locator(`table[role="grid"] tbody tr td[aria-colindex="${colIndex + 1}"]`)
60
+ .first();
61
+ // Wait for the cell to attach. The first probe is short (800ms fast path); after
62
+ // any nudge the probe uses the full remaining budget so Glide has time to re-render.
63
+ // Total ceiling = 800ms + attachTimeout, matching the original two-phase behaviour.
64
+ const deadline = Date.now() + 800 + attachTimeout;
65
+ let nudged = false;
66
+ while (true) {
67
+ const remaining = deadline - Date.now();
68
+ if (remaining <= 0)
69
+ throw Object.assign(new Error(`Timed out after ${attachTimeout}ms waiting for aria-colindex="${colIndex + 1}" to attach`), { name: 'TimeoutError' });
70
+ try {
71
+ await target.waitFor({ state: 'attached', timeout: nudged ? remaining : Math.min(800, remaining) });
72
+ break;
73
+ }
74
+ catch (err) {
75
+ if (!(err instanceof Error) || err.name !== 'TimeoutError')
76
+ throw err;
77
+ await scroller.evaluate((el) => {
78
+ el.scrollLeft = Math.min(el.scrollLeft + el.clientWidth, el.scrollWidth);
79
+ });
80
+ nudged = true;
81
+ }
82
+ }
83
+ },
84
+ scrollToRow: async ({ page }, rowIndex) => {
85
+ const scroller = page.locator('.dvn-scroller').first();
86
+ // aria-rowindex is 2-based for data rows (header = 1)
87
+ const ariaRowIndex = rowIndex + 2;
88
+ const rowSel = `table[role="grid"] tbody tr[aria-rowindex="${ariaRowIndex}"]`;
89
+ // If the row is already mounted, scroll it into view directly.
90
+ const isAttached = await page.locator(rowSel).count() > 0;
91
+ if (isAttached) {
92
+ await page.locator(rowSel).first().scrollIntoViewIfNeeded();
93
+ }
94
+ else {
95
+ // Measure actual row height from the DOM; fall back to Glide's default (34px).
96
+ const rowHeight = await page
97
+ .locator('table[role="grid"] tbody tr')
98
+ .first()
99
+ .evaluate((el) => el.getBoundingClientRect().height)
100
+ .catch(() => 34);
101
+ const height = rowHeight > 0 ? rowHeight : 34;
102
+ await scroller.evaluate((el, { idx, height: h }) => {
103
+ const targetTop = idx * h;
104
+ el.scrollTop = Math.max(0, targetTop - Math.floor(el.clientHeight / 2));
105
+ }, { idx: rowIndex, height });
106
+ await page
107
+ .locator(rowSel)
108
+ .waitFor({ state: 'attached', timeout: attachTimeout });
109
+ }
110
+ },
111
+ };
112
+ };
113
+ exports.createGlideViewport = createGlideViewport;
@@ -1,3 +1,4 @@
1
1
  export { muiTable, muiDataGrid } from './mui';
2
2
  export { RDG as rdg } from './rdg';
3
- export { Glide as glide } from './glide';
3
+ export { Glide as glide, createGlide, createGlideViewport } from './glide';
4
+ export type { GlideOptions, GlideViewportOptions } from './glide';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.glide = exports.rdg = exports.muiDataGrid = exports.muiTable = void 0;
3
+ exports.createGlideViewport = exports.createGlide = exports.glide = exports.rdg = exports.muiDataGrid = exports.muiTable = void 0;
4
4
  var mui_1 = require("./mui");
5
5
  Object.defineProperty(exports, "muiTable", { enumerable: true, get: function () { return mui_1.muiTable; } });
6
6
  Object.defineProperty(exports, "muiDataGrid", { enumerable: true, get: function () { return mui_1.muiDataGrid; } });
@@ -8,3 +8,5 @@ var rdg_1 = require("./rdg");
8
8
  Object.defineProperty(exports, "rdg", { enumerable: true, get: function () { return rdg_1.RDG; } });
9
9
  var glide_1 = require("./glide");
10
10
  Object.defineProperty(exports, "glide", { enumerable: true, get: function () { return glide_1.Glide; } });
11
+ Object.defineProperty(exports, "createGlide", { enumerable: true, get: function () { return glide_1.createGlide; } });
12
+ Object.defineProperty(exports, "createGlideViewport", { enumerable: true, get: function () { return glide_1.createGlideViewport; } });
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.muiDataGrid = exports.muiTable = void 0;
4
+ const debugUtils_1 = require("../utils/debugUtils");
4
5
  /**
5
6
  * Safely wait for MUI pagination to stabilize.
6
7
  * Polls for displayed row text changes and waits for loading overlays to disappear.
@@ -86,13 +87,25 @@ exports.muiTable = {
86
87
  const root = await getPaginationRoot(context);
87
88
  const prevBtn = root.locator('button[aria-label="Go to previous page"]');
88
89
  const displayedRows = root.locator('.MuiTablePagination-displayedRows');
89
- let maxRetries = 50;
90
90
  let clicked = false;
91
- while (maxRetries-- > 0 && await prevBtn.count() > 0 && !(await prevBtn.isDisabled())) {
91
+ let noProgressCount = 0;
92
+ const NO_PROGRESS_LIMIT = 3;
93
+ while (await prevBtn.count() > 0 && !(await prevBtn.isDisabled())) {
92
94
  const oldText = await displayedRows.innerText().catch(() => '');
93
95
  await prevBtn.click();
94
96
  await waitForMuiPaginationStabilization(context, displayedRows, oldText);
95
97
  clicked = true;
98
+ const newText = await displayedRows.innerText().catch(() => '');
99
+ if (newText === oldText) {
100
+ noProgressCount++;
101
+ if (noProgressCount >= NO_PROGRESS_LIMIT) {
102
+ (0, debugUtils_1.logDebug)(context.config, 'error', `goToFirst: no pagination progress after ${NO_PROGRESS_LIMIT} consecutive clicks — aborting to avoid infinite loop`);
103
+ return false;
104
+ }
105
+ }
106
+ else {
107
+ noProgressCount = 0;
108
+ }
96
109
  }
97
110
  return clicked;
98
111
  }