@rickcedwhat/playwright-smart-table 2.0.0 → 2.0.1

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.
@@ -84,16 +84,18 @@ exports.TableStrategies = {
84
84
  const oldCount = yield rows.count();
85
85
  if (oldCount === 0)
86
86
  return false;
87
- // 1. Scroll the very last row into view to trigger the fetch
88
- yield rows.last().scrollIntoViewIfNeeded();
89
- // Optional: Press "End" to force scroll on stubborn grids (like AG Grid)
90
- yield page.keyboard.press('End');
91
- // 2. Smart Wait: Wait for row count to increase
87
+ // Aggressive Scroll Logic:
88
+ // We use expect.poll to RETRY the scroll action if the count hasn't increased.
89
+ // This fixes flakiness where the first scroll might be missed by the intersection observer.
92
90
  try {
93
- yield (0, test_1.expect)(() => __awaiter(void 0, void 0, void 0, function* () {
94
- const newCount = yield rows.count();
95
- (0, test_1.expect)(newCount).toBeGreaterThan(oldCount);
96
- })).toPass({ timeout });
91
+ yield test_1.expect.poll(() => __awaiter(void 0, void 0, void 0, function* () {
92
+ // 1. Trigger: Scroll the last row into view
93
+ yield rows.last().scrollIntoViewIfNeeded();
94
+ // 2. Force: Press "End" to help with stubborn window-scrollers
95
+ yield page.keyboard.press('End');
96
+ // 3. Return count for assertion
97
+ return rows.count();
98
+ }), { timeout }).toBeGreaterThan(oldCount);
97
99
  return true;
98
100
  }
99
101
  catch (e) {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * šŸ¤– AUTO-GENERATED FILE. DO NOT EDIT.
3
+ * This file is generated by scripts/embed-types.js
4
+ * It contains the raw text of types.ts to provide context for LLM prompts.
5
+ */
6
+ export declare const TYPE_CONTEXT = "\n// src/types.ts\n\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\nexport type SmartRow = Locator & {\n getCell(column: string): Locator;\n toJSON(): Promise<Record<string, string>>;\n};\n\nexport interface TableContext {\n root: Locator;\n config: Required<TableConfig>;\n page: Page;\n resolve: (selector: Selector, parent: Locator | Page) => Locator;\n}\n\n/**\n * A function that handles pagination logic.\n * Returns true if more data was loaded (navigation occurred), false if end of data.\n */\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport interface PromptOptions {\n /** * Where to output the prompt.\n * - 'console': Logs to stdout (default).\n * - 'report': Attaches to the Playwright HTML report (best for CI/VMs).\n * - 'file': Writes to a local file (e.g. 'smart-table-prompt.md').\n */\n output?: 'console' | 'report' | 'file';\n \n /**\n * Include TypeScript interfaces in the prompt?\n * Helps LLMs generate correct code structure.\n * Default: true\n */\n includeTypes?: boolean;\n}\n\nexport interface TableConfig {\n rowSelector?: Selector;\n headerSelector?: Selector;\n cellSelector?: Selector;\n /**\n * Strategy for handling pagination.\n * Use presets from TableStrategies or write your own.\n */\n pagination?: PaginationStrategy;\n maxPages?: number;\n /**\n * Optional hook to rename columns dynamically.\n * Useful for naming empty columns (like '__col_0') to something semantic like 'Actions'.\n */\n headerTransformer?: (text: string, index: number) => string;\n /**\n * Automatically scroll the table into view on first interaction.\n * Helps ensure the table is visible in traces/videos.\n * Default: true\n */\n autoScroll?: boolean;\n}\n\nexport interface TableResult {\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\n\n /** * Find a specific row by its content.\n * Default: Returns SmartRow (Locator).\n * Option { asJSON: true }: Returns Record<string, string> (Data).\n */\n getByRow: <T extends { asJSON?: boolean }>(\n filters: Record<string, string | RegExp | number>, \n options?: { exact?: boolean, maxPages?: number } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;\n\n /** * Get all rows on the current page.\n * Can be filtered by column values.\n * * @param options.asJSON - Returns data objects instead of SmartRows\n * @param options.filter - Key-Value pairs to filter rows (e.g. { Status: 'Active' })\n * @param options.exact - Exact match for filters (default: false)\n */\n getAllRows: <T extends { asJSON?: boolean }>(\n options?: { \n filter?: Record<string, string | RegExp | number>; \n exact?: boolean \n } & T\n ) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;\n\n generateConfigPrompt: (options?: PromptOptions) => Promise<void>;\n generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;\n}\n";
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TYPE_CONTEXT = void 0;
4
+ /**
5
+ * šŸ¤– AUTO-GENERATED FILE. DO NOT EDIT.
6
+ * This file is generated by scripts/embed-types.js
7
+ * It contains the raw text of types.ts to provide context for LLM prompts.
8
+ */
9
+ exports.TYPE_CONTEXT = `
10
+ // src/types.ts
11
+
12
+ export type Selector = string | ((root: Locator | Page) => Locator);
13
+
14
+ export type SmartRow = Locator & {
15
+ getCell(column: string): Locator;
16
+ toJSON(): Promise<Record<string, string>>;
17
+ };
18
+
19
+ export interface TableContext {
20
+ root: Locator;
21
+ config: Required<TableConfig>;
22
+ page: Page;
23
+ resolve: (selector: Selector, parent: Locator | Page) => Locator;
24
+ }
25
+
26
+ /**
27
+ * A function that handles pagination logic.
28
+ * Returns true if more data was loaded (navigation occurred), false if end of data.
29
+ */
30
+ export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
31
+
32
+ export interface PromptOptions {
33
+ /** * Where to output the prompt.
34
+ * - 'console': Logs to stdout (default).
35
+ * - 'report': Attaches to the Playwright HTML report (best for CI/VMs).
36
+ * - 'file': Writes to a local file (e.g. 'smart-table-prompt.md').
37
+ */
38
+ output?: 'console' | 'report' | 'file';
39
+
40
+ /**
41
+ * Include TypeScript interfaces in the prompt?
42
+ * Helps LLMs generate correct code structure.
43
+ * Default: true
44
+ */
45
+ includeTypes?: boolean;
46
+ }
47
+
48
+ export interface TableConfig {
49
+ rowSelector?: Selector;
50
+ headerSelector?: Selector;
51
+ cellSelector?: Selector;
52
+ /**
53
+ * Strategy for handling pagination.
54
+ * Use presets from TableStrategies or write your own.
55
+ */
56
+ pagination?: PaginationStrategy;
57
+ maxPages?: number;
58
+ /**
59
+ * Optional hook to rename columns dynamically.
60
+ * Useful for naming empty columns (like '__col_0') to something semantic like 'Actions'.
61
+ */
62
+ headerTransformer?: (text: string, index: number) => string;
63
+ /**
64
+ * Automatically scroll the table into view on first interaction.
65
+ * Helps ensure the table is visible in traces/videos.
66
+ * Default: true
67
+ */
68
+ autoScroll?: boolean;
69
+ }
70
+
71
+ export interface TableResult {
72
+ getHeaders: () => Promise<string[]>;
73
+ getHeaderCell: (columnName: string) => Promise<Locator>;
74
+
75
+ /** * Find a specific row by its content.
76
+ * Default: Returns SmartRow (Locator).
77
+ * Option { asJSON: true }: Returns Record<string, string> (Data).
78
+ */
79
+ getByRow: <T extends { asJSON?: boolean }>(
80
+ filters: Record<string, string | RegExp | number>,
81
+ options?: { exact?: boolean, maxPages?: number } & T
82
+ ) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
83
+
84
+ /** * Get all rows on the current page.
85
+ * Can be filtered by column values.
86
+ * * @param options.asJSON - Returns data objects instead of SmartRows
87
+ * @param options.filter - Key-Value pairs to filter rows (e.g. { Status: 'Active' })
88
+ * @param options.exact - Exact match for filters (default: false)
89
+ */
90
+ getAllRows: <T extends { asJSON?: boolean }>(
91
+ options?: {
92
+ filter?: Record<string, string | RegExp | number>;
93
+ exact?: boolean
94
+ } & T
95
+ ) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
96
+
97
+ generateConfigPrompt: (options?: PromptOptions) => Promise<void>;
98
+ generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;
99
+ }
100
+ `;
package/dist/types.d.ts CHANGED
@@ -15,6 +15,20 @@ export interface TableContext {
15
15
  * Returns true if more data was loaded (navigation occurred), false if end of data.
16
16
  */
17
17
  export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
18
+ export interface PromptOptions {
19
+ /** * Where to output the prompt.
20
+ * - 'console': Logs to stdout (default).
21
+ * - 'report': Attaches to the Playwright HTML report (best for CI/VMs).
22
+ * - 'file': Writes to a local file (e.g. 'smart-table-prompt.md').
23
+ */
24
+ output?: 'console' | 'report' | 'file';
25
+ /**
26
+ * Include TypeScript interfaces in the prompt?
27
+ * Helps LLMs generate correct code structure.
28
+ * Default: true
29
+ */
30
+ includeTypes?: boolean;
31
+ }
18
32
  export interface TableConfig {
19
33
  rowSelector?: Selector;
20
34
  headerSelector?: Selector;
@@ -30,6 +44,12 @@ export interface TableConfig {
30
44
  * Useful for naming empty columns (like '__col_0') to something semantic like 'Actions'.
31
45
  */
32
46
  headerTransformer?: (text: string, index: number) => string;
47
+ /**
48
+ * Automatically scroll the table into view on first interaction.
49
+ * Helps ensure the table is visible in traces/videos.
50
+ * Default: true
51
+ */
52
+ autoScroll?: boolean;
33
53
  }
34
54
  export interface TableResult {
35
55
  getHeaders: () => Promise<string[]>;
@@ -45,12 +65,17 @@ export interface TableResult {
45
65
  maxPages?: number;
46
66
  } & T) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
47
67
  /** * Get all rows on the current page.
48
- * Default: Returns SmartRow[] (Locators).
49
- * Option { asJSON: true }: Returns Record<string, string>[] (Data).
68
+ * Can be filtered by column values.
69
+ * * @param options.asJSON - Returns data objects instead of SmartRows
70
+ * @param options.filter - Key-Value pairs to filter rows (e.g. { Status: 'Active' })
71
+ * @param options.exact - Exact match for filters (default: false)
50
72
  */
51
73
  getAllRows: <T extends {
52
74
  asJSON?: boolean;
53
- }>(options?: T) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
54
- generateConfigPrompt: () => Promise<void>;
55
- generateStrategyPrompt: () => Promise<void>;
75
+ }>(options?: {
76
+ filter?: Record<string, string | RegExp | number>;
77
+ exact?: boolean;
78
+ } & T) => Promise<T['asJSON'] extends true ? Record<string, string>[] : SmartRow[]>;
79
+ generateConfigPrompt: (options?: PromptOptions) => Promise<void>;
80
+ generateStrategyPrompt: (options?: PromptOptions) => Promise<void>;
56
81
  }
package/dist/useTable.js CHANGED
@@ -10,8 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.useTable = void 0;
13
+ const test_1 = require("@playwright/test");
14
+ const typeContext_1 = require("./typeContext");
13
15
  const useTable = (rootLocator, configOptions = {}) => {
14
- const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: undefined, maxPages: 1, headerTransformer: undefined }, configOptions);
16
+ const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: undefined, maxPages: 1, headerTransformer: undefined, autoScroll: true }, configOptions);
15
17
  const resolve = (item, parent) => {
16
18
  if (typeof item === 'string')
17
19
  return parent.locator(item);
@@ -23,6 +25,10 @@ const useTable = (rootLocator, configOptions = {}) => {
23
25
  const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
24
26
  if (_headerMap)
25
27
  return _headerMap;
28
+ // āœ… New Feature: Auto-Scroll on first interaction
29
+ if (config.autoScroll) {
30
+ yield rootLocator.scrollIntoViewIfNeeded();
31
+ }
26
32
  const headerLoc = resolve(config.headerSelector, rootLocator);
27
33
  try {
28
34
  yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
@@ -63,39 +69,69 @@ const useTable = (rootLocator, configOptions = {}) => {
63
69
  });
64
70
  return smart;
65
71
  };
72
+ // ā™»ļø HELPER: Centralized logic to filter a row locator
73
+ const _applyFilters = (baseRows, filters, map, exact) => {
74
+ let filtered = baseRows;
75
+ const page = rootLocator.page();
76
+ for (const [colName, value] of Object.entries(filters)) {
77
+ const colIndex = map.get(colName);
78
+ if (colIndex === undefined)
79
+ throw new Error(`Column '${colName}' not found.`);
80
+ const filterVal = typeof value === 'number' ? String(value) : value;
81
+ const cellTemplate = resolve(config.cellSelector, page);
82
+ // Filter the TRs that contain the matching cell at the specific index
83
+ filtered = filtered.filter({
84
+ has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
85
+ });
86
+ }
87
+ return filtered;
88
+ };
89
+ const _handlePrompt = (promptName_1, content_1, ...args_1) => __awaiter(void 0, [promptName_1, content_1, ...args_1], void 0, function* (promptName, content, options = {}) {
90
+ const { output = 'console', includeTypes = true } = options; // Default includeTypes to true
91
+ let finalPrompt = content;
92
+ if (includeTypes) {
93
+ // āœ… Inject the dynamic TYPE_CONTEXT
94
+ finalPrompt += `\n\nšŸ‘‡ Useful TypeScript Definitions šŸ‘‡\n\`\`\`typescript\n${typeContext_1.TYPE_CONTEXT}\n\`\`\`\n`;
95
+ }
96
+ if (output === 'console') {
97
+ console.log(finalPrompt);
98
+ }
99
+ else if (output === 'report') {
100
+ if (test_1.test.info()) {
101
+ yield test_1.test.info().attach(promptName, {
102
+ body: finalPrompt,
103
+ contentType: 'text/markdown'
104
+ });
105
+ console.log(`āœ… Attached '${promptName}' to Playwright Report.`);
106
+ }
107
+ else {
108
+ console.warn('āš ļø Cannot attach to report: No active test info found.');
109
+ console.log(finalPrompt);
110
+ }
111
+ }
112
+ // ... (file output logic) ...
113
+ });
66
114
  const _findRowLocator = (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
67
115
  var _a;
68
116
  const map = yield _getMap();
69
- const page = rootLocator.page();
70
117
  const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
71
118
  let currentPage = 1;
72
119
  while (true) {
73
- let rowLocator = resolve(config.rowSelector, rootLocator);
74
- for (const [colName, value] of Object.entries(filters)) {
75
- const colIndex = map.get(colName);
76
- if (colIndex === undefined)
77
- throw new Error(`Column '${colName}' not found. Available: ${[...map.keys()].join(', ')}`);
78
- const exact = options.exact || false;
79
- const filterVal = typeof value === 'number' ? String(value) : value;
80
- const cellTemplate = resolve(config.cellSelector, page);
81
- rowLocator = rowLocator.filter({
82
- has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
83
- });
84
- }
85
- const count = yield rowLocator.count();
120
+ // 1. Get all rows
121
+ const allRows = resolve(config.rowSelector, rootLocator);
122
+ // 2. Apply filters using helper
123
+ const matchedRows = _applyFilters(allRows, filters, map, options.exact || false);
124
+ // 3. Check Count
125
+ const count = yield matchedRows.count();
86
126
  if (count > 1)
87
127
  throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
88
128
  if (count === 1)
89
- return rowLocator.first();
129
+ return matchedRows.first();
130
+ // 4. Pagination Logic (unchanged)
90
131
  if (config.pagination && currentPage < effectiveMaxPages) {
91
- const context = {
92
- root: rootLocator,
93
- config: config,
94
- page: page,
95
- resolve: resolve
96
- };
97
- const didLoadMore = yield config.pagination(context);
98
- if (didLoadMore) {
132
+ // ... (pagination code same as before)
133
+ const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
134
+ if (yield config.pagination(context)) {
99
135
  currentPage++;
100
136
  continue;
101
137
  }
@@ -128,24 +164,30 @@ const useTable = (rootLocator, configOptions = {}) => {
128
164
  }),
129
165
  getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
130
166
  const map = yield _getMap();
131
- const rowLocators = yield resolve(config.rowSelector, rootLocator).all();
132
- const smartRows = rowLocators.map(loc => _makeSmart(loc, map));
167
+ let rowLocators = resolve(config.rowSelector, rootLocator);
168
+ // āœ… NEW: Apply filters if they exist
169
+ if (options === null || options === void 0 ? void 0 : options.filter) {
170
+ rowLocators = _applyFilters(rowLocators, options.filter, map, options.exact || false);
171
+ }
172
+ // Convert Locator to array of Locators
173
+ const rows = yield rowLocators.all();
174
+ const smartRows = rows.map(loc => _makeSmart(loc, map));
133
175
  if (options === null || options === void 0 ? void 0 : options.asJSON) {
134
176
  return Promise.all(smartRows.map(r => r.toJSON()));
135
177
  }
136
178
  return smartRows;
137
179
  }),
138
- generateConfigPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
180
+ generateConfigPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
139
181
  const html = yield rootLocator.evaluate((el) => el.outerHTML);
140
182
  const separator = "=".repeat(50);
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);
183
+ const content = `\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`;
184
+ yield _handlePrompt('Smart Table Config', content, options);
143
185
  }),
144
- generateStrategyPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
186
+ generateStrategyPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
145
187
  const container = rootLocator.locator('xpath=..');
146
188
  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`;
148
- console.log(prompt);
189
+ const content = `\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`;
190
+ yield _handlePrompt('Smart Table Strategy', content, options);
149
191
  })
150
192
  };
151
193
  };
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@rickcedwhat/playwright-smart-table",
3
- "version": "2.0.0",
4
- "description": "A smart table utility for Playwright with built-in pagination strategies.",
3
+ "version": "2.0.1",
4
+ "description": "A smart table utility for Playwright with built-in pagination strategies that are fully extensible.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
8
  "dist"
9
9
  ],
10
10
  "scripts": {
11
- "build": "tsc",
11
+ "generate-types": "node scripts/embed-types.mjs",
12
+ "build": "npm run generate-types && tsc",
12
13
  "prepublishOnly": "npm run build",
13
- "publish-public": "npm publish --access public",
14
+ "release": "npm run build && npm publish --access public",
14
15
  "test": "npx playwright test"
15
16
  },
16
17
  "keywords": [