@rickcedwhat/playwright-smart-table 6.12.0 → 6.14.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
@@ -4,6 +4,7 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/github/package-json/v/rickcedwhat/playwright-smart-table?label=npm&color=blue&t=2)](https://www.npmjs.com/package/@rickcedwhat/playwright-smart-table)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![CodeRabbit Reviews](https://img.shields.io/coderabbit/prs/github/rickcedwhat/playwright-smart-table?utm_source=oss&utm_medium=github&utm_campaign=rickcedwhat%2Fplaywright-smart-table&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)](https://coderabbit.ai)
7
8
 
8
9
  ## 📚 [Full Documentation →](https://rickcedwhat.github.io/playwright-smart-table/)
9
10
 
@@ -45,6 +46,7 @@ await table.forEach(async ({ row }) => {
45
46
  npm install @rickcedwhat/playwright-smart-table
46
47
  ```
47
48
 
49
+
48
50
  ## Quick Start
49
51
 
50
52
  ```typescript
@@ -14,7 +14,6 @@ export declare class RowFinder<T = any> {
14
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;
@@ -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();
@@ -45,11 +42,11 @@ class RowFinder {
45
42
  const allRows = [];
46
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;
47
44
  let pagesScanned = 1;
48
- 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)})`);
49
46
  const tracker = new elementTracker_1.ElementTracker('findRows');
50
47
  try {
51
48
  const collectMatches = async () => {
52
- var _a, _b;
49
+ var _a, _b, _c, _d, _e;
53
50
  let rowLocators = this.resolve(this.config.rowSelector, this.rootLocator);
54
51
  // Only apply filters if we have them
55
52
  if (Object.keys(filtersRecord).length > 0) {
@@ -59,17 +56,47 @@ class RowFinder {
59
56
  const newIndices = await tracker.getUnseenIndices(rowLocators);
60
57
  const currentRows = await rowLocators.all();
61
58
  const isRowLoading = (_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isRowLoading;
59
+ const rawRowTimeout = (_c = this.config.strategies.loading) === null || _c === void 0 ? void 0 : _c.rowLoadingTimeout;
60
+ // undefined = unset (legacy skip); 0 = no wait (immediate check); >0 = wait up to N ms
61
+ // Non-finite values are treated as unset (defensive guard against Infinity/NaN)
62
+ const rowLoadingTimeout = rawRowTimeout !== undefined && Number.isFinite(rawRowTimeout) && rawRowTimeout >= 0
63
+ ? rawRowTimeout
64
+ : undefined;
65
+ const onRowLoadingTimeout = (_e = (_d = this.config.strategies.loading) === null || _d === void 0 ? void 0 : _d.onRowLoadingTimeout) !== null && _e !== void 0 ? _e : 'read-as-is';
62
66
  let added = 0;
63
67
  for (const idx of newIndices) {
64
68
  const smartRow = this.makeSmartRow(currentRows[idx], map, allRows.length, this.tableState.currentPageIndex);
65
69
  if (isRowLoading && await isRowLoading(smartRow)) {
66
- this.log(`findRows: page ${this.tableState.currentPageIndex} — row skipped (isRowLoading=true)`);
67
- continue;
70
+ if (rowLoadingTimeout === undefined) {
71
+ // No timeout configured (or invalid value) — legacy skip behavior
72
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: page ${this.tableState.currentPageIndex} — row skipped (isRowLoading=true, no timeout)`);
73
+ continue;
74
+ }
75
+ // Wait up to rowLoadingTimeout ms (0 = one immediate re-check, no polling)
76
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: row ${allRows.length} — waiting up to ${rowLoadingTimeout}ms for row to load`);
77
+ const deadline = Date.now() + rowLoadingTimeout;
78
+ let resolved = !(await isRowLoading(smartRow)); // immediate check (handles timeout=0)
79
+ while (!resolved && Date.now() < deadline) {
80
+ await currentRows[idx].page().waitForTimeout(100);
81
+ resolved = !(await isRowLoading(smartRow));
82
+ }
83
+ if (!resolved) {
84
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: row ${allRows.length} — still loading after ${rowLoadingTimeout}ms, action: ${onRowLoadingTimeout}`);
85
+ if (onRowLoadingTimeout === 'skip')
86
+ continue;
87
+ if (onRowLoadingTimeout === 'throw') {
88
+ throw new Error(`[SmartTable] Row ${allRows.length} did not finish loading within ${rowLoadingTimeout}ms`);
89
+ }
90
+ // 'read-as-is': fall through and add the row
91
+ }
92
+ else {
93
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: row ${allRows.length} — finished loading`);
94
+ }
68
95
  }
69
96
  allRows.push(smartRow);
70
97
  added++;
71
98
  }
72
- this.log(`findRows: page ${this.tableState.currentPageIndex} — ${added} new match(es) (total: ${allRows.length})`);
99
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: page ${this.tableState.currentPageIndex} — ${added} new match(es) (total: ${allRows.length})`);
73
100
  };
74
101
  // Scan first page
75
102
  await collectMatches();
@@ -90,25 +117,26 @@ class RowFinder {
90
117
  paginationResult = await this.config.strategies.pagination.goNext(context);
91
118
  }
92
119
  else {
93
- this.log(`findRows: no pagination primitive — stopping`);
120
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: no pagination primitive — stopping`);
94
121
  break;
95
122
  }
96
123
  const didPaginate = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
97
124
  if (!didPaginate) {
98
- this.log(`findRows: pagination returned false — end of data`);
125
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: pagination returned false — end of data`);
99
126
  break;
100
127
  }
101
128
  const pagesJumped = typeof paginationResult === 'number' ? paginationResult : 1;
102
129
  this.tableState.currentPageIndex += pagesJumped;
103
130
  pagesScanned += pagesJumped;
104
- this.log(`findRows: advanced ${pagesJumped} page(s), now at page ${this.tableState.currentPageIndex}`);
131
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: advanced ${pagesJumped} page(s), now at page ${this.tableState.currentPageIndex}`);
132
+ await (0, debugUtils_1.debugDelay)(this.config, 'pagination');
105
133
  await collectMatches();
106
134
  }
107
135
  }
108
136
  finally {
109
137
  await tracker.cleanup(this.rootLocator.page());
110
138
  }
111
- this.log(`findRows: done — ${allRows.length} row(s) collected across ${pagesScanned} page(s)`);
139
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRows: done — ${allRows.length} row(s) collected across ${pagesScanned} page(s)`);
112
140
  return (0, smartRowArray_1.createSmartRowArray)(allRows);
113
141
  }
114
142
  async findRowLocator(filters, options = {}) {
@@ -116,7 +144,7 @@ class RowFinder {
116
144
  const map = await this.tableMapper.getMap();
117
145
  const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : this.config.maxPages;
118
146
  let pagesScanned = 1;
119
- this.log(`Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
147
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `Looking for row: ${JSON.stringify(filters)} (MaxPages: ${effectiveMaxPages})`);
120
148
  while (true) {
121
149
  // Check Loading
122
150
  if ((_b = this.config.strategies.loading) === null || _b === void 0 ? void 0 : _b.isTableLoading) {
@@ -127,7 +155,7 @@ class RowFinder {
127
155
  resolve: this.resolve
128
156
  });
129
157
  if (isLoading) {
130
- this.log('Table is loading... waiting');
158
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', 'Table is loading... waiting');
131
159
  await this.rootLocator.page().waitForTimeout(200);
132
160
  continue;
133
161
  }
@@ -135,7 +163,7 @@ class RowFinder {
135
163
  const allRows = this.resolve(this.config.rowSelector, this.rootLocator);
136
164
  const matchedRows = this.filterEngine.applyFilters(allRows, filters, map, options.exact || false, this.rootLocator.page(), this.rootLocator);
137
165
  const count = await matchedRows.count();
138
- this.log(`Page ${this.tableState.currentPageIndex}: Found ${count} matches.`);
166
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Found ${count} matches.`);
139
167
  if (count > 1) {
140
168
  const sampleData = [];
141
169
  try {
@@ -154,7 +182,7 @@ class RowFinder {
154
182
  if (count === 1)
155
183
  return matchedRows.first();
156
184
  if (pagesScanned < effectiveMaxPages) {
157
- this.log(`Page ${this.tableState.currentPageIndex}: Not found. Attempting pagination...`);
185
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Not found. Attempting pagination...`);
158
186
  const context = {
159
187
  root: this.rootLocator,
160
188
  config: this.config,
@@ -171,7 +199,7 @@ class RowFinder {
171
199
  paginationResult = await pagination.goNext(context);
172
200
  }
173
201
  else {
174
- this.log(`Page ${this.tableState.currentPageIndex}: Pagination failed (no goNext or goNextBulk primitive).`);
202
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Pagination failed (no goNext or goNextBulk primitive).`);
175
203
  return null;
176
204
  }
177
205
  const didLoadMore = (0, validation_1.validatePaginationResult)(paginationResult, 'Pagination Strategy');
@@ -179,10 +207,12 @@ class RowFinder {
179
207
  const pagesJumped = typeof paginationResult === 'number' ? paginationResult : 1;
180
208
  this.tableState.currentPageIndex += pagesJumped;
181
209
  pagesScanned += pagesJumped;
210
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `findRowLocator: advanced ${pagesJumped} page(s), now at page ${this.tableState.currentPageIndex}`);
211
+ await (0, debugUtils_1.debugDelay)(this.config, 'pagination');
182
212
  continue;
183
213
  }
184
214
  else {
185
- this.log(`Page ${this.tableState.currentPageIndex}: Pagination failed (end of data).`);
215
+ (0, debugUtils_1.logDebug)(this.config, 'verbose', `Page ${this.tableState.currentPageIndex}: Pagination failed (end of data).`);
186
216
  }
187
217
  }
188
218
  return null;
@@ -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).
@@ -67,7 +67,7 @@ async function runMap(env, callback, options = {}, label = 'map') {
67
67
  log(env.config, `${label}: scanning page ${pagesScanned} — ${batchSize} new row(s)`);
68
68
  const barrier = useBarrier ? new navigationBarrier_1.NavigationBarrier(batchSize) : undefined;
69
69
  const actionMutex = useMutex ? new mutex_1.Mutex() : null;
70
- 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));
71
71
  const processRow = async (row) => {
72
72
  try {
73
73
  if (stopped && row.rowIndex > stoppedIndex) {
@@ -86,7 +86,7 @@ async function runMap(env, callback, options = {}, label = 'map') {
86
86
  if (stopped && row.rowIndex > stoppedIndex)
87
87
  return SKIP;
88
88
  log(env.config, `${label}: processing row ${row.rowIndex}`);
89
- 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) });
90
90
  };
91
91
  if (actionMutex) {
92
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.12.0";
6
+ export declare const PLAYWRIGHT_SMART_TABLE_VERSION: "6.14.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.12.0";
9
+ exports.PLAYWRIGHT_SMART_TABLE_VERSION = "6.14.0";
@@ -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
  },
@@ -102,11 +97,29 @@ const GlideDefaultStrategies = {
102
97
  getActiveCell: glideGetActiveCell
103
98
  };
104
99
  /** Strategies only for Glide Data Grid. Includes fillSimple; use when you want to supply your own selectors or override fill. */
100
+ // fallow-ignore-next-line unused-export
105
101
  exports.GlideStrategies = Object.assign(Object.assign({}, GlideDefaultStrategies), { fillSimple: glideFillSimple });
102
+ /**
103
+ * Factory that returns a configured Glide preset.
104
+ * Use when your grid differs from the defaults:
105
+ * ```ts
106
+ * useTable(loc, { ...createGlide({ attachTimeout: 5000 }), maxPages: 5 })
107
+ * ```
108
+ */
109
+ function createGlide(options = {}) {
110
+ return {
111
+ headerSelector: 'table[role="grid"] thead tr th',
112
+ rowSelector: 'table[role="grid"] tbody tr',
113
+ cellSelector: 'td',
114
+ concurrency: 'sequential',
115
+ strategies: Object.assign(Object.assign({}, GlideDefaultStrategies), { viewport: (0, viewport_2.createGlideViewport)(options) })
116
+ };
117
+ }
106
118
  /**
107
119
  * Full preset for Glide Data Grid (selectors + default strategies only).
108
120
  * Spread: useTable(loc, { ...Plugins.Glide, maxPages: 5 }).
109
121
  * Strategies only (including fillSimple): useTable(loc, { rowSelector: '...', strategies: Plugins.Glide.Strategies }).
122
+ * For a non-64-column grid, use `createGlide({ columnCount })` instead.
110
123
  */
111
124
  const GlidePreset = {
112
125
  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; } });
@@ -100,6 +100,7 @@ const RDGDefaultStrategies = {
100
100
  pagination: rdgPaginationStrategy
101
101
  };
102
102
  /** Full strategies for React Data Grid. Use when you want to supply your own selectors: strategies: Plugins.RDG.Strategies */
103
+ // fallow-ignore-next-line unused-export
103
104
  exports.RDGStrategies = RDGDefaultStrategies;
104
105
  /**
105
106
  * Full preset for React Data Grid (selectors + default strategies).