@rickcedwhat/playwright-smart-table 1.0.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,118 +1,152 @@
1
1
  Playwright Smart Table 🧠
2
+
2
3
  A production-ready, type-safe table wrapper for Playwright.
3
- It handles the hard stuff automatically:
4
- Pagination (Next buttons, Load More, Infinite Scroll)
5
- Complex Grids (MUI, AG-Grid, React Table)
6
- Strict Mode (Throws errors if your filters match multiple rows)
7
- šŸš€ Installation
8
- npm install playwright-smart-table
9
4
 
5
+ This library abstracts away the complexity of testing dynamic web tables. It handles Pagination, Infinite Scroll, Virtualization, and Data Grids (MUI, AG-Grid) so your tests remain clean and readable.
10
6
 
11
- (Note: Requires @playwright/test as a peer dependency)
12
- šŸ Quick Start
13
- Standard HTML Table (No config needed)
14
- import { test, expect } from '@playwright/test';
15
- import { useTable } from 'playwright-smart-table';
7
+ šŸ“¦ Installation
16
8
 
17
- test('Verify User', async ({ page }) => {
18
- await page.goto('/users');
9
+ npm install @rickcedwhat/playwright-smart-table
19
10
 
20
- // 1. Initialize (Defaults to <table>, <tr>, <td>)
21
- const table = useTable(page.locator('#users-table'));
22
-
23
- // 2. Find row across pages automatically!
24
- // This will search Page 1, then Page 2, etc.
25
- const row = await table.getByRow({ Name: 'Alice', Role: 'Admin' });
26
-
27
- await expect(row).toBeVisible();
28
- });
29
11
 
12
+ Requires @playwright/test as a peer dependency.
30
13
 
31
- 🧩 Pagination Strategies
32
- This library doesn't guess how your table works. You tell it using a Strategy.
33
- 1. Standard "Next" Button
34
- For tables like Datatables.net or simple paginated lists.
35
- import { TableStrategies } from 'playwright-smart-table';
36
-
37
- const table = useTable(locator, {
38
- // Strategy: Find button -> Click -> Wait for first row to change
39
- pagination: TableStrategies.clickNext('[aria-label="Next Page"]')
40
- });
14
+ šŸš€ Quick Start
41
15
 
16
+ 1. The Standard HTML Table
42
17
 
43
- 2. Infinite Scroll
44
- For grids that load more data as you scroll down (AG-Grid, Virtual Lists, HTMX).
45
- const table = useTable(locator, {
46
- // Strategy: Aggressive scroll to bottom -> Wait for row count to increase
47
- pagination: TableStrategies.infiniteScroll()
48
- });
18
+ For standard tables (<table>, <tr>, <td>), no configuration is needed.
49
19
 
20
+ import { test, expect } from '@playwright/test';
21
+ import { useTable } from '@rickcedwhat/playwright-smart-table';
50
22
 
51
- 3. Load More Button
52
- For lists with a "Load More Results" button at the bottom.
53
- const table = useTable(locator, {
54
- // Strategy: Click button -> Wait for row count to increase
55
- pagination: TableStrategies.clickLoadMore('button.load-more')
23
+ test('Verify User Email', async ({ page }) => {
24
+ const table = useTable(page.locator('#users-table'));
25
+
26
+ // šŸŖ„ Magic: Finds the row with Name="Alice", then gets the Email cell
27
+ // If Alice is on Page 2, it handles pagination automatically.
28
+ await expect(
29
+ await table.getByCell({ Name: 'Alice' }, 'Email')
30
+ ).toHaveText('alice@example.com');
56
31
  });
57
32
 
58
33
 
59
- āš™ļø Advanced Config (MUI / Grid / Divs)
60
- For complex div-based tables (like Material UI DataGrid), you can override the selectors.
34
+ 2. Complex Grids (Material UI / AG-Grid / Divs)
35
+
36
+ For modern React grids that use <div> structures, simply override the selectors.
37
+
38
+ import { useTable, TableStrategies } from '@rickcedwhat/playwright-smart-table';
39
+
61
40
  const table = useTable(page.locator('.MuiDataGrid-root'), {
62
41
  rowSelector: '.MuiDataGrid-row',
63
42
  headerSelector: '.MuiDataGrid-columnHeader',
64
43
  cellSelector: '.MuiDataGrid-cell',
65
- pagination: TableStrategies.clickNext('[aria-label="Go to next page"]')
44
+ // Strategy: Tell it how to find the next page
45
+ pagination: TableStrategies.clickNext(
46
+ (root) => root.getByRole('button', { name: 'Go to next page' })
47
+ )
66
48
  });
67
49
 
68
50
 
51
+ 🧩 Pagination Strategies
52
+
53
+ This library uses the Strategy Pattern to handle navigation. This ensures future stability: we can add new ways to paginate without breaking existing tests.
54
+
55
+ clickNext(selector)
56
+
57
+ Best for standard tables (Datatables, lists).
58
+
59
+ Behavior: Clicks the button -> Waits for the first row of data to change.
60
+
61
+ Selector: Can be a CSS string OR a Playwright locator function.
62
+
63
+ // CSS String
64
+ pagination: TableStrategies.clickNext('button.next-page')
65
+
66
+ // Locator Function (More Robust)
67
+ pagination: TableStrategies.clickNext((root) => root.getByRole('button', { name: 'Next' }))
68
+
69
+
70
+ infiniteScroll()
71
+
72
+ Best for Virtualized Grids (AG-Grid) or lazy-loading lists (HTMX).
73
+
74
+ Behavior: Aggressively scrolls the container/window to the bottom -> Waits for row count to increase.
75
+
76
+ pagination: TableStrategies.infiniteScroll()
77
+
78
+
79
+ clickLoadMore(selector)
80
+
81
+ Best for "Load More" buttons.
82
+
83
+ Behavior: Clicks button -> Waits for row count to increase.
84
+
85
+ pagination: TableStrategies.clickLoadMore('button.load-more')
86
+
87
+
69
88
  šŸ“– API Reference
89
+
70
90
  getByRow(filters, options?)
71
- Finds a specific row matching the filters.
72
- filters: { "Column Name": "Value", "Age": "25" }
73
- options: { exact: true }
74
- Returns: Playwright Locator for that row.
75
- Throws error if multiple rows match.
76
- await table.getByRow({ Email: "alice@example.com" });
91
+
92
+ Returns the Locator for a specific row.
93
+
94
+ Strict Mode: Throws an error if filters match more than 1 row.
95
+
96
+ Auto-Pagination: Will search up to maxPages to find the row.
97
+
98
+ // Find a row where Name is "Alice" AND Role is "Admin"
99
+ const row = await table.getByRow({ Name: "Alice", Role: "Admin" });
100
+ await expect(row).toBeVisible();
77
101
 
78
102
 
79
103
  getByCell(filters, targetColumn)
80
- Finds a specific cell inside a filtered row. Useful for clicking action buttons.
81
- Returns: Playwright Locator for the cell.
82
- // Find the 'Edit' button for Alice
104
+
105
+ Returns the Locator for a specific cell inside a matched row.
106
+
107
+ Use this for interactions (clicking edit buttons, checking checkboxes).
108
+
109
+ // Find Alice's row, then find the "Actions" column, then click the button inside it
83
110
  await table.getByCell({ Name: "Alice" }, "Actions").getByRole('button').click();
84
111
 
85
112
 
113
+ getRowAsJSON(filters)
114
+
115
+ Returns a POJO (Plain Old JavaScript Object) of the row data. Useful for debugging or strict data assertions.
116
+
117
+ const data = await table.getRowAsJSON({ ID: "101" });
118
+ console.log(data);
119
+ // Output: { ID: "101", Name: "Alice", Status: "Active" }
120
+
121
+
86
122
  getRows()
87
- Dumps all rows on the current page as an array of objects. Great for verifying sort order.
88
- const rows = await table.getRows();
89
- expect(rows[0].Name).toBe("Alice");
90
- expect(rows[1].Name).toBe("Bob");
91
123
 
124
+ Returns an array of all rows on the current page.
92
125
 
93
- getRowAsJSON(filters)
94
- Returns a single row's data as a JSON object.
95
- const data = await table.getRowAsJSON({ ID: "123" });
96
- console.log(data); // { ID: "123", Name: "Alice", Status: "Active" }
126
+ const allRows = await table.getRows();
127
+ expect(allRows[0].Name).toBe("Alice"); // Verify sort order
97
128
 
98
129
 
99
- šŸ› ļø Custom Strategies
100
- Need to handle a custom pagination logic? Write your own strategy!
101
- A strategy is just a function that returns Promise<boolean> (true if data loaded, false if done).
102
- const myCustomStrategy = async ({ root, page, resolve }) => {
103
- // 1. Find your specific button
104
- const btn = resolve('.my-weird-button', root);
105
-
106
- if (!await btn.isVisible()) return false; // Stop pagination
107
-
108
- // 2. Click and Wait
109
- await btn.click();
110
- await page.waitForResponse(resp => resp.url().includes('/api/users'));
111
-
112
- return true; // We loaded more!
113
- };
130
+ šŸ› ļø Developer Tools
131
+
132
+ The library includes helper tools to generate configurations for you.
133
+
134
+ // Print the HTML structure prompt to console
135
+ // Copy-paste the output into ChatGPT/Gemini to get your config object
136
+ await table.generateConfigPrompt();
137
+
138
+ // Print a prompt to help write a custom Pagination Strategy
139
+ await table.generateStrategyPrompt();
140
+
141
+
142
+ šŸ›”ļø Stability & Versioning
143
+
144
+ This package follows Semantic Versioning.
145
+
146
+ 1.x.x: No breaking changes to the useTable signature.
114
147
 
115
- // Usage
116
- useTable(locator, { pagination: myCustomStrategy });
148
+ New strategies may be added, but existing ones will remain stable.
117
149
 
150
+ To ensure stability in your projects, install with:
118
151
 
152
+ "@rickcedwhat/playwright-smart-table": "^1.0.0"
@@ -0,0 +1,18 @@
1
+ import { Locator } from '@playwright/test';
2
+ import { TableConfig } from './types';
3
+ /**
4
+ * Preset for Key-Value Forms.
5
+ * * Default Structure:
6
+ * - Row: div.form-group
7
+ * - Cell: Direct children (> *)
8
+ * - Columns: ['Label', 'Input']
9
+ */
10
+ export declare const useForm: (rootLocator: Locator, options?: TableConfig) => import("./types").TableResult;
11
+ /**
12
+ * Preset for Navigation Menus.
13
+ * * Default Structure:
14
+ * - Row: li
15
+ * - Cell: null (The row IS the cell)
16
+ * - Columns: ['Item']
17
+ */
18
+ export declare const useMenu: (menuLocator: Locator, options?: TableConfig) => import("./types").TableResult;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useMenu = exports.useForm = void 0;
4
+ const useTable_1 = require("./useTable");
5
+ /**
6
+ * Preset for Key-Value Forms.
7
+ * * Default Structure:
8
+ * - Row: div.form-group
9
+ * - Cell: Direct children (> *)
10
+ * - Columns: ['Label', 'Input']
11
+ */
12
+ const useForm = (rootLocator, options = {}) => {
13
+ return (0, useTable_1.useTable)(rootLocator, Object.assign({
14
+ // Defaults:
15
+ rowSelector: 'div.form-group', cellSelector: (row) => row.locator('> *'), headerSelector: null, columnNames: ['Label', 'Input'] }, options));
16
+ };
17
+ exports.useForm = useForm;
18
+ /**
19
+ * Preset for Navigation Menus.
20
+ * * Default Structure:
21
+ * - Row: li
22
+ * - Cell: null (The row IS the cell)
23
+ * - Columns: ['Item']
24
+ */
25
+ const useMenu = (menuLocator, options = {}) => {
26
+ return (0, useTable_1.useTable)(menuLocator, Object.assign({
27
+ // Defaults:
28
+ rowSelector: 'li', cellSelector: null, headerSelector: null, columnNames: ['Item'] }, options));
29
+ };
30
+ exports.useMenu = useMenu;
package/dist/types.d.ts CHANGED
@@ -1,20 +1,56 @@
1
1
  import { Locator, Page } from '@playwright/test';
2
+ export type Selector = string | ((root: Locator | Page) => Locator);
3
+ export type SmartRow = Locator & {
4
+ getCell(column: string): Locator;
5
+ toJSON(): Promise<Record<string, string>>;
6
+ };
7
+ export interface TableContext {
8
+ root: Locator;
9
+ config: Required<TableConfig>;
10
+ page: Page;
11
+ resolve: (selector: Selector, parent: Locator | Page) => Locator;
12
+ }
2
13
  /**
3
- * A selector can be a CSS string or a function.
4
- * We allow 'parent' to be Locator OR Page to match your working logic.
14
+ * A function that handles pagination logic.
15
+ * Returns true if more data was loaded (navigation occurred), false if end of data.
5
16
  */
6
- export type Selector = string | ((parent: Locator | Page) => Locator);
17
+ export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
7
18
  export interface TableConfig {
8
19
  rowSelector?: Selector;
9
20
  headerSelector?: Selector;
10
21
  cellSelector?: Selector;
22
+ /**
23
+ * Strategy for handling pagination.
24
+ * Use presets from TableStrategies or write your own.
25
+ */
11
26
  pagination?: PaginationStrategy;
12
27
  maxPages?: number;
28
+ /**
29
+ * Optional hook to rename columns dynamically.
30
+ * Useful for naming empty columns (like '__col_0') to something semantic like 'Actions'.
31
+ */
32
+ headerTransformer?: (text: string, index: number) => string;
13
33
  }
14
- export interface TableContext {
15
- root: Locator;
16
- config: Required<TableConfig>;
17
- page: Page;
18
- resolve: (item: Selector, parent: Locator | Page) => Locator;
34
+ export interface TableResult {
35
+ getHeaders: () => Promise<string[]>;
36
+ getHeaderCell: (columnName: string) => Promise<Locator>;
37
+ /** * Find a specific row by its content.
38
+ * Default: Returns SmartRow (Locator).
39
+ * Option { asJSON: true }: Returns Record<string, string> (Data).
40
+ */
41
+ getByRow: <T extends {
42
+ asJSON?: boolean;
43
+ }>(filters: Record<string, string | RegExp | number>, options?: {
44
+ exact?: boolean;
45
+ maxPages?: number;
46
+ } & T) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
47
+ /** * Get all rows on the current page.
48
+ * Default: Returns SmartRow[] (Locators).
49
+ * Option { asJSON: true }: Returns Record<string, string>[] (Data).
50
+ */
51
+ getAllRows: <T extends {
52
+ asJSON?: boolean;
53
+ }>(options?: T) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
54
+ generateConfigPrompt: () => Promise<void>;
55
+ generateStrategyPrompt: () => Promise<void>;
19
56
  }
20
- export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
@@ -1,17 +1,3 @@
1
1
  import { Locator } from '@playwright/test';
2
- import { TableConfig } from './types';
3
- export declare const useTable: (rootLocator: Locator, configOptions?: TableConfig) => {
4
- getHeaders: () => Promise<string[]>;
5
- getByRow: (filters: Record<string, string | RegExp | number>, options?: {
6
- exact?: boolean;
7
- maxPages?: number;
8
- }) => Promise<Locator>;
9
- getByCell: (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => Promise<Locator>;
10
- getRows: () => Promise<Record<string, string>[]>;
11
- getRowAsJSON: (filters: Record<string, string | RegExp | number>) => Promise<Record<string, string>>;
12
- /**
13
- * šŸ› ļø DEV TOOL: Prints a prompt to the console.
14
- * Copy the output and paste it into Gemini/ChatGPT to generate your config.
15
- */
16
- generateConfigPrompt: () => Promise<void>;
17
- };
2
+ import { TableConfig, TableResult } from './types';
3
+ export declare const useTable: (rootLocator: Locator, configOptions?: TableConfig) => TableResult;
package/dist/useTable.js CHANGED
@@ -11,8 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.useTable = void 0;
13
13
  const useTable = (rootLocator, configOptions = {}) => {
14
- const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: undefined, maxPages: 1 }, configOptions);
15
- // āœ… UPDATE: Accept Locator OR Page (to match your work logic)
14
+ const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: undefined, maxPages: 1, headerTransformer: undefined }, configOptions);
16
15
  const resolve = (item, parent) => {
17
16
  if (typeof item === 'string')
18
17
  return parent.locator(item);
@@ -24,16 +23,46 @@ const useTable = (rootLocator, configOptions = {}) => {
24
23
  const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
25
24
  if (_headerMap)
26
25
  return _headerMap;
27
- // Headers are still resolved relative to the table root (safer)
28
26
  const headerLoc = resolve(config.headerSelector, rootLocator);
29
27
  try {
30
28
  yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
31
29
  }
32
30
  catch (e) { /* Ignore hydration */ }
33
31
  const texts = yield headerLoc.allInnerTexts();
34
- _headerMap = new Map(texts.map((t, i) => [t.trim() || `__col_${i}`, i]));
32
+ _headerMap = new Map(texts.map((t, i) => {
33
+ let text = t.trim() || `__col_${i}`;
34
+ if (config.headerTransformer)
35
+ text = config.headerTransformer(text, i);
36
+ return [text, i];
37
+ }));
35
38
  return _headerMap;
36
39
  });
40
+ const _makeSmart = (rowLocator, map) => {
41
+ const smart = rowLocator;
42
+ smart.getCell = (colName) => {
43
+ const idx = map.get(colName);
44
+ if (idx === undefined)
45
+ throw new Error(`Column '${colName}' not found.`);
46
+ if (typeof config.cellSelector === 'string') {
47
+ return rowLocator.locator(config.cellSelector).nth(idx);
48
+ }
49
+ else {
50
+ return resolve(config.cellSelector, rowLocator).nth(idx);
51
+ }
52
+ };
53
+ smart.toJSON = () => __awaiter(void 0, void 0, void 0, function* () {
54
+ const result = {};
55
+ const cells = typeof config.cellSelector === 'string'
56
+ ? rowLocator.locator(config.cellSelector)
57
+ : resolve(config.cellSelector, rowLocator);
58
+ const texts = yield cells.allInnerTexts();
59
+ for (const [col, idx] of map.entries()) {
60
+ result[col] = (texts[idx] || '').trim();
61
+ }
62
+ return result;
63
+ });
64
+ return smart;
65
+ };
37
66
  const _findRowLocator = (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
38
67
  var _a;
39
68
  const map = yield _getMap();
@@ -41,29 +70,23 @@ const useTable = (rootLocator, configOptions = {}) => {
41
70
  const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
42
71
  let currentPage = 1;
43
72
  while (true) {
44
- // 1. Row Locator uses ROOT (Matches your snippet)
45
73
  let rowLocator = resolve(config.rowSelector, rootLocator);
46
74
  for (const [colName, value] of Object.entries(filters)) {
47
75
  const colIndex = map.get(colName);
48
76
  if (colIndex === undefined)
49
- throw new Error(`Column '${colName}' not found.`);
77
+ throw new Error(`Column '${colName}' not found. Available: ${[...map.keys()].join(', ')}`);
50
78
  const exact = options.exact || false;
51
79
  const filterVal = typeof value === 'number' ? String(value) : value;
52
- // āœ… MATCHING YOUR WORK LOGIC EXACTLY
53
- // 2. Cell Template uses PAGE (Matches your snippet)
54
80
  const cellTemplate = resolve(config.cellSelector, page);
55
- // 3. Filter using .nth(colIndex)
56
81
  rowLocator = rowLocator.filter({
57
82
  has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
58
83
  });
59
84
  }
60
85
  const count = yield rowLocator.count();
61
- if (count > 1) {
86
+ if (count > 1)
62
87
  throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
63
- }
64
88
  if (count === 1)
65
89
  return rowLocator.first();
66
- // --- PAGINATION LOGIC ---
67
90
  if (config.pagination && currentPage < effectiveMaxPages) {
68
91
  const context = {
69
92
  root: rootLocator,
@@ -82,145 +105,48 @@ const useTable = (rootLocator, configOptions = {}) => {
82
105
  });
83
106
  return {
84
107
  getHeaders: () => __awaiter(void 0, void 0, void 0, function* () { return Array.from((yield _getMap()).keys()); }),
85
- getByRow: (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
86
- const row = yield _findRowLocator(filters, options);
87
- if (!row)
88
- return resolve(config.rowSelector, rootLocator).filter({ hasText: "NON_EXISTENT_ROW_SENTINEL_" + Date.now() });
89
- return row;
90
- }),
91
- getByCell: (rowFilters, targetColumn) => __awaiter(void 0, void 0, void 0, function* () {
92
- const row = yield _findRowLocator(rowFilters);
93
- if (!row)
94
- throw new Error(`Row not found: ${JSON.stringify(rowFilters)}`);
95
- const map = yield _getMap();
96
- const colIndex = map.get(targetColumn);
97
- if (colIndex === undefined)
98
- throw new Error(`Column '${targetColumn}' not found.`);
99
- // Return the specific cell
100
- // We scope this to the found ROW to ensure we get the right cell
101
- if (typeof config.cellSelector === 'string') {
102
- return row.locator(config.cellSelector).nth(colIndex);
103
- }
104
- else {
105
- return resolve(config.cellSelector, row).nth(colIndex);
106
- }
107
- }),
108
- getRows: () => __awaiter(void 0, void 0, void 0, function* () {
108
+ getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
109
109
  const map = yield _getMap();
110
- const rowLocator = resolve(config.rowSelector, rootLocator);
111
- const rowCount = yield rowLocator.count();
112
- const results = [];
113
- for (let i = 0; i < rowCount; i++) {
114
- const row = rowLocator.nth(i);
115
- let cells;
116
- if (typeof config.cellSelector === 'string') {
117
- cells = row.locator(config.cellSelector);
118
- }
119
- else {
120
- cells = resolve(config.cellSelector, row);
121
- }
122
- const cellTexts = yield cells.allInnerTexts();
123
- const rowData = {};
124
- for (const [colName, colIdx] of map.entries()) {
125
- rowData[colName] = (cellTexts[colIdx] || "").trim();
126
- }
127
- results.push(rowData);
128
- }
129
- return results;
110
+ const idx = map.get(columnName);
111
+ if (idx === undefined)
112
+ throw new Error(`Column '${columnName}' not found.`);
113
+ return resolve(config.headerSelector, rootLocator).nth(idx);
130
114
  }),
131
- getRowAsJSON: (filters) => __awaiter(void 0, void 0, void 0, function* () {
132
- const row = yield _findRowLocator(filters);
133
- if (!row)
134
- throw new Error(`Row not found: ${JSON.stringify(filters)}`);
135
- let cells;
136
- if (typeof config.cellSelector === 'string') {
137
- cells = row.locator(config.cellSelector);
115
+ getByRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
116
+ let row = yield _findRowLocator(filters, options);
117
+ // āœ… FIX: Sentinel Logic for negative assertions (expect(row).not.toBeVisible())
118
+ if (!row) {
119
+ row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
138
120
  }
139
- else {
140
- cells = resolve(config.cellSelector, row);
121
+ const smartRow = _makeSmart(row, yield _getMap());
122
+ if (options === null || options === void 0 ? void 0 : options.asJSON) {
123
+ // If row doesn't exist, toJSON() returns empty object or throws?
124
+ // For safety, let's let it run naturally (it will likely return empty strings)
125
+ return smartRow.toJSON();
141
126
  }
142
- const cellTexts = yield cells.allInnerTexts();
127
+ return smartRow;
128
+ }),
129
+ getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
143
130
  const map = yield _getMap();
144
- const result = {};
145
- for (const [colName, colIndex] of map.entries()) {
146
- result[colName] = (cellTexts[colIndex] || "").trim();
131
+ const rowLocators = yield resolve(config.rowSelector, rootLocator).all();
132
+ const smartRows = rowLocators.map(loc => _makeSmart(loc, map));
133
+ if (options === null || options === void 0 ? void 0 : options.asJSON) {
134
+ return Promise.all(smartRows.map(r => r.toJSON()));
147
135
  }
148
- return result;
136
+ return smartRows;
149
137
  }),
150
- /**
151
- * šŸ› ļø DEV TOOL: Prints a prompt to the console.
152
- * Copy the output and paste it into Gemini/ChatGPT to generate your config.
153
- */
154
138
  generateConfigPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
155
139
  const html = yield rootLocator.evaluate((el) => el.outerHTML);
156
140
  const separator = "=".repeat(50);
157
- const prompt = `
158
- ${separator}
159
- šŸ¤– COPY THE TEXT BELOW INTO GEMINI/ChatGPT šŸ¤–
160
- ${separator}
161
-
162
- I am using a Playwright helper factory called 'useTable'.
163
- I need you to generate the configuration object based on the HTML structure below.
164
-
165
- Here is the table HTML:
166
- \`\`\`html
167
- ${html}
168
- \`\`\`
169
-
170
- Based on this HTML, generate the configuration object matching this signature:
171
- const table = useTable(page.locator('...'), {
172
- // Find the rows (exclude headers and empty spacer rows if possible)
173
- rowSelector: "...", // OR (root) => root.locator(...)
174
-
175
- // Find the column headers
176
- headerSelector: "...", // OR (root) => root.locator(...)
177
-
178
- // Find the cell (relative to a specific row)
179
- cellSelector: "...", // OR (row) => row.locator(...)
180
-
181
- // Find the "Next Page" button (if it exists in the HTML)
182
- paginationNextSelector: (root) => root.locator(...)
183
- });
184
-
185
- **Requirements:**
186
- 1. Prefer \`getByRole\` or \`getByTestId\` over CSS classes where possible.
187
- 2. If the table uses \`div\` structures (like React Table), ensure the \`rowSelector\` does not accidentally select the header row.
188
- 3. If there are "padding" or "loading" rows, use \`.filter()\` to exclude them.
189
-
190
- ${separator}
191
- `;
141
+ const prompt = `\n${separator}\nšŸ¤– COPY INTO GEMINI/ChatGPT šŸ¤–\n${separator}\nI am using 'playwright-smart-table'. Generate config for:\n\`\`\`html\n${html.substring(0, 5000)} ...\n\`\`\`\n${separator}\n`;
142
+ console.log(prompt);
143
+ }),
144
+ generateStrategyPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
145
+ const container = rootLocator.locator('xpath=..');
146
+ const html = yield container.evaluate((el) => el.outerHTML);
147
+ const prompt = `\n==================================================\nšŸ¤– COPY INTO GEMINI/ChatGPT TO WRITE A STRATEGY šŸ¤–\n==================================================\nI need a custom Pagination Strategy for 'playwright-smart-table'.\nContainer HTML:\n\`\`\`html\n${html.substring(0, 5000)} ...\n\`\`\`\n`;
192
148
  console.log(prompt);
193
149
  })
194
150
  };
195
- /**
196
- * šŸ› ļø DEV TOOL: Prints a prompt to help write a custom Pagination Strategy.
197
- * It snapshots the HTML *surrounding* the table to find buttons/scroll containers.
198
- */
199
- generateStrategyPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
200
- // 1. Get the parent container (often holds the pagination controls)
201
- const container = rootLocator.locator('xpath=..');
202
- const html = yield container.evaluate((el) => el.outerHTML);
203
- const prompt = `
204
- ==================================================
205
- šŸ¤– COPY INTO GEMINI/ChatGPT TO WRITE A STRATEGY šŸ¤–
206
- ==================================================
207
-
208
- I am using 'playwright-smart-table'. I need a custom Pagination Strategy.
209
- The table is inside this container HTML:
210
-
211
- \`\`\`html
212
- ${html.substring(0, 5000)} ... (truncated)
213
- \`\`\`
214
-
215
- Write a strategy that implements this interface:
216
- type PaginationStrategy = (context: TableContext) => Promise<boolean>;
217
-
218
- Requirements:
219
- 1. Identify the "Next" button OR the scroll container.
220
- 2. Return 'true' if data loaded, 'false' if end of data.
221
- 3. Use context.resolve() to find elements.
222
- `;
223
- console.log(prompt);
224
- });
225
151
  };
226
152
  exports.useTable = useTable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "1.0.6",
3
+ "version": "2.0.0",
4
4
  "description": "A smart table utility for Playwright with built-in pagination strategies.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,14 +10,19 @@
10
10
  "scripts": {
11
11
  "build": "tsc",
12
12
  "prepublishOnly": "npm run build",
13
- "publish": "npm publish --access public",
13
+ "publish-public": "npm publish --access public",
14
14
  "test": "npx playwright test"
15
15
  },
16
- "keywords": ["playwright", "testing", "table", "automation"],
16
+ "keywords": [
17
+ "playwright",
18
+ "testing",
19
+ "table",
20
+ "automation"
21
+ ],
17
22
  "author": "",
18
23
  "license": "ISC",
19
24
  "peerDependencies": {
20
- "@playwright/test": "*"
25
+ "@playwright/test": "*"
21
26
  },
22
27
  "peerDependenciesMeta": {
23
28
  "@playwright/test": {