@rickcedwhat/playwright-smart-table 1.0.7 ā 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.
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/presets.d.ts +2 -26
- package/dist/strategies/index.js +11 -9
- package/dist/typeContext.d.ts +6 -0
- package/dist/typeContext.js +100 -0
- package/dist/types.d.ts +68 -13
- package/dist/useTable.d.ts +2 -14
- package/dist/useTable.js +131 -165
- package/package.json +12 -6
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/presets.d.ts
CHANGED
|
@@ -7,19 +7,7 @@ import { TableConfig } from './types';
|
|
|
7
7
|
* - Cell: Direct children (> *)
|
|
8
8
|
* - Columns: ['Label', 'Input']
|
|
9
9
|
*/
|
|
10
|
-
export declare const useForm: (rootLocator: Locator, options?: TableConfig) =>
|
|
11
|
-
getHeaders: () => Promise<string[]>;
|
|
12
|
-
getByRow: (filters: Record<string, string | RegExp | number>, options?: {
|
|
13
|
-
exact?: boolean;
|
|
14
|
-
maxPages?: number;
|
|
15
|
-
}) => Promise<Locator>;
|
|
16
|
-
getByCell: (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => Promise<Locator>;
|
|
17
|
-
getRows: () => Promise<Record<string, string>[]>;
|
|
18
|
-
getRowAsJSON: (filters: Record<string, string | RegExp | number>) => Promise<Record<string, string>>;
|
|
19
|
-
setColumnName: (colIndex: number, newNameOrFn: string | ((current: string) => string)) => Promise<void>;
|
|
20
|
-
generateConfigPrompt: () => Promise<void>;
|
|
21
|
-
generateStrategyPrompt: () => Promise<void>;
|
|
22
|
-
};
|
|
10
|
+
export declare const useForm: (rootLocator: Locator, options?: TableConfig) => import("./types").TableResult;
|
|
23
11
|
/**
|
|
24
12
|
* Preset for Navigation Menus.
|
|
25
13
|
* * Default Structure:
|
|
@@ -27,16 +15,4 @@ export declare const useForm: (rootLocator: Locator, options?: TableConfig) => {
|
|
|
27
15
|
* - Cell: null (The row IS the cell)
|
|
28
16
|
* - Columns: ['Item']
|
|
29
17
|
*/
|
|
30
|
-
export declare const useMenu: (menuLocator: Locator, options?: TableConfig) =>
|
|
31
|
-
getHeaders: () => Promise<string[]>;
|
|
32
|
-
getByRow: (filters: Record<string, string | RegExp | number>, options?: {
|
|
33
|
-
exact?: boolean;
|
|
34
|
-
maxPages?: number;
|
|
35
|
-
}) => Promise<Locator>;
|
|
36
|
-
getByCell: (rowFilters: Record<string, string | RegExp | number>, targetColumn: string) => Promise<Locator>;
|
|
37
|
-
getRows: () => Promise<Record<string, string>[]>;
|
|
38
|
-
getRowAsJSON: (filters: Record<string, string | RegExp | number>) => Promise<Record<string, string>>;
|
|
39
|
-
setColumnName: (colIndex: number, newNameOrFn: string | ((current: string) => string)) => Promise<void>;
|
|
40
|
-
generateConfigPrompt: () => Promise<void>;
|
|
41
|
-
generateStrategyPrompt: () => Promise<void>;
|
|
42
|
-
};
|
|
18
|
+
export declare const useMenu: (menuLocator: Locator, options?: TableConfig) => import("./types").TableResult;
|
package/dist/strategies/index.js
CHANGED
|
@@ -84,16 +84,18 @@ exports.TableStrategies = {
|
|
|
84
84
|
const oldCount = yield rows.count();
|
|
85
85
|
if (oldCount === 0)
|
|
86
86
|
return false;
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
//
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
@@ -1,26 +1,81 @@
|
|
|
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
|
|
14
|
+
* A function that handles pagination logic.
|
|
15
|
+
* Returns true if more data was loaded (navigation occurred), false if end of data.
|
|
4
16
|
*/
|
|
5
|
-
export type
|
|
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
|
+
}
|
|
6
32
|
export interface TableConfig {
|
|
7
33
|
rowSelector?: Selector;
|
|
8
34
|
headerSelector?: Selector;
|
|
9
35
|
cellSelector?: Selector;
|
|
36
|
+
/**
|
|
37
|
+
* Strategy for handling pagination.
|
|
38
|
+
* Use presets from TableStrategies or write your own.
|
|
39
|
+
*/
|
|
10
40
|
pagination?: PaginationStrategy;
|
|
11
41
|
maxPages?: number;
|
|
12
42
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
43
|
+
* Optional hook to rename columns dynamically.
|
|
44
|
+
* Useful for naming empty columns (like '__col_0') to something semantic like 'Actions'.
|
|
45
|
+
*/
|
|
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
|
|
17
51
|
*/
|
|
18
|
-
|
|
52
|
+
autoScroll?: boolean;
|
|
19
53
|
}
|
|
20
|
-
export interface
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
54
|
+
export interface TableResult {
|
|
55
|
+
getHeaders: () => Promise<string[]>;
|
|
56
|
+
getHeaderCell: (columnName: string) => Promise<Locator>;
|
|
57
|
+
/** * Find a specific row by its content.
|
|
58
|
+
* Default: Returns SmartRow (Locator).
|
|
59
|
+
* Option { asJSON: true }: Returns Record<string, string> (Data).
|
|
60
|
+
*/
|
|
61
|
+
getByRow: <T extends {
|
|
62
|
+
asJSON?: boolean;
|
|
63
|
+
}>(filters: Record<string, string | RegExp | number>, options?: {
|
|
64
|
+
exact?: boolean;
|
|
65
|
+
maxPages?: number;
|
|
66
|
+
} & T) => Promise<T['asJSON'] extends true ? Record<string, string> : SmartRow>;
|
|
67
|
+
/** * Get all rows on the current page.
|
|
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)
|
|
72
|
+
*/
|
|
73
|
+
getAllRows: <T extends {
|
|
74
|
+
asJSON?: boolean;
|
|
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>;
|
|
25
81
|
}
|
|
26
|
-
export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
package/dist/useTable.d.ts
CHANGED
|
@@ -1,15 +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
|
-
setColumnName: (colIndex: number, newNameOrFn: string | ((current: string) => string)) => Promise<void>;
|
|
13
|
-
generateConfigPrompt: () => Promise<void>;
|
|
14
|
-
generateStrategyPrompt: () => Promise<void>;
|
|
15
|
-
};
|
|
2
|
+
import { TableConfig, TableResult } from './types';
|
|
3
|
+
export declare const useTable: (rootLocator: Locator, configOptions?: TableConfig) => TableResult;
|
package/dist/useTable.js
CHANGED
|
@@ -10,108 +10,127 @@ 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,
|
|
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);
|
|
18
20
|
if (typeof item === 'function')
|
|
19
21
|
return item(parent);
|
|
20
|
-
|
|
22
|
+
return item;
|
|
21
23
|
};
|
|
22
24
|
let _headerMap = null;
|
|
23
25
|
const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
26
|
if (_headerMap)
|
|
25
27
|
return _headerMap;
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const headerLoc = resolve(config.headerSelector, rootLocator);
|
|
30
|
-
try {
|
|
31
|
-
yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
|
|
32
|
-
texts = yield headerLoc.allInnerTexts();
|
|
33
|
-
}
|
|
34
|
-
catch (e) { /* Ignore hydration/empty/timeout */ }
|
|
28
|
+
// ā
New Feature: Auto-Scroll on first interaction
|
|
29
|
+
if (config.autoScroll) {
|
|
30
|
+
yield rootLocator.scrollIntoViewIfNeeded();
|
|
35
31
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const colCount = Math.max(texts.length, overrides.length);
|
|
40
|
-
for (let i = 0; i < colCount; i++) {
|
|
41
|
-
const scrapedText = (texts[i] || "").trim() || `__col_${i}`;
|
|
42
|
-
const overrideText = overrides[i];
|
|
43
|
-
const finalName = (overrideText !== undefined) ? overrideText : scrapedText;
|
|
44
|
-
_headerMap.set(finalName, i);
|
|
32
|
+
const headerLoc = resolve(config.headerSelector, rootLocator);
|
|
33
|
+
try {
|
|
34
|
+
yield headerLoc.first().waitFor({ state: 'visible', timeout: 3000 });
|
|
45
35
|
}
|
|
36
|
+
catch (e) { /* Ignore hydration */ }
|
|
37
|
+
const texts = yield headerLoc.allInnerTexts();
|
|
38
|
+
_headerMap = new Map(texts.map((t, i) => {
|
|
39
|
+
let text = t.trim() || `__col_${i}`;
|
|
40
|
+
if (config.headerTransformer)
|
|
41
|
+
text = config.headerTransformer(text, i);
|
|
42
|
+
return [text, i];
|
|
43
|
+
}));
|
|
46
44
|
return _headerMap;
|
|
47
45
|
});
|
|
46
|
+
const _makeSmart = (rowLocator, map) => {
|
|
47
|
+
const smart = rowLocator;
|
|
48
|
+
smart.getCell = (colName) => {
|
|
49
|
+
const idx = map.get(colName);
|
|
50
|
+
if (idx === undefined)
|
|
51
|
+
throw new Error(`Column '${colName}' not found.`);
|
|
52
|
+
if (typeof config.cellSelector === 'string') {
|
|
53
|
+
return rowLocator.locator(config.cellSelector).nth(idx);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
return resolve(config.cellSelector, rowLocator).nth(idx);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
smart.toJSON = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
60
|
+
const result = {};
|
|
61
|
+
const cells = typeof config.cellSelector === 'string'
|
|
62
|
+
? rowLocator.locator(config.cellSelector)
|
|
63
|
+
: resolve(config.cellSelector, rowLocator);
|
|
64
|
+
const texts = yield cells.allInnerTexts();
|
|
65
|
+
for (const [col, idx] of map.entries()) {
|
|
66
|
+
result[col] = (texts[idx] || '').trim();
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
});
|
|
70
|
+
return smart;
|
|
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
|
+
});
|
|
48
114
|
const _findRowLocator = (filters_1, ...args_1) => __awaiter(void 0, [filters_1, ...args_1], void 0, function* (filters, options = {}) {
|
|
49
115
|
var _a;
|
|
50
116
|
const map = yield _getMap();
|
|
51
|
-
const page = rootLocator.page();
|
|
52
117
|
const effectiveMaxPages = (_a = options.maxPages) !== null && _a !== void 0 ? _a : config.maxPages;
|
|
53
118
|
let currentPage = 1;
|
|
54
119
|
while (true) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
throw new Error(`Column '${colName}' not found. Available: ${Array.from(map.keys())}`);
|
|
62
|
-
const exact = options.exact || false;
|
|
63
|
-
const filterVal = typeof value === 'number' ? String(value) : value;
|
|
64
|
-
// Case 1: No Cell Selector (Menu) - Filter the Row Itself
|
|
65
|
-
if (!config.cellSelector) {
|
|
66
|
-
if (exact) {
|
|
67
|
-
rowLocator = rowLocator.filter({ hasText: new RegExp(`^${escapeRegExp(String(filterVal))}$`) });
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
rowLocator = rowLocator.filter({ hasText: filterVal });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// Case 2: String Cell Selector - Standard Table Logic (Restored)
|
|
74
|
-
else if (typeof config.cellSelector === 'string') {
|
|
75
|
-
// RESTORED: This logic worked for standard tables.
|
|
76
|
-
// We resolve against the PAGE to create a generic locator template.
|
|
77
|
-
// Playwright handles the relative filtering correctly for standard tables.
|
|
78
|
-
const cellTemplate = resolve(config.cellSelector, page);
|
|
79
|
-
rowLocator = rowLocator.filter({
|
|
80
|
-
has: cellTemplate.nth(colIndex).getByText(filterVal, { exact }),
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
// Case 3: Function Cell Selector - Forms (Iterative Fallback)
|
|
84
|
-
else {
|
|
85
|
-
const count = yield rowLocator.count();
|
|
86
|
-
let matchFound = false;
|
|
87
|
-
for (let i = 0; i < count; i++) {
|
|
88
|
-
const specificRow = rowLocator.nth(i);
|
|
89
|
-
// Resolve cell relative to this specific row
|
|
90
|
-
const specificCell = config.cellSelector(specificRow).nth(colIndex);
|
|
91
|
-
if ((yield specificCell.getByText(filterVal, { exact }).count()) > 0) {
|
|
92
|
-
if (matchFound) {
|
|
93
|
-
throw new Error(`Strict Mode Violation: Found multiple rows matching ${JSON.stringify(filters)}.`);
|
|
94
|
-
}
|
|
95
|
-
rowLocator = specificRow;
|
|
96
|
-
matchFound = true;
|
|
97
|
-
// Break inner loop to proceed to next filter or return
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (!matchFound) {
|
|
102
|
-
// Return empty locator to fail gracefully
|
|
103
|
-
return resolve(config.rowSelector, rootLocator).filter({ hasText: "NON_EXISTENT_ROW_" + Date.now() });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
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();
|
|
108
126
|
if (count > 1)
|
|
109
127
|
throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
|
|
110
128
|
if (count === 1)
|
|
111
|
-
return
|
|
112
|
-
//
|
|
129
|
+
return matchedRows.first();
|
|
130
|
+
// 4. Pagination Logic (unchanged)
|
|
113
131
|
if (config.pagination && currentPage < effectiveMaxPages) {
|
|
114
|
-
|
|
132
|
+
// ... (pagination code same as before)
|
|
133
|
+
const context = { root: rootLocator, config, page: rootLocator.page(), resolve };
|
|
115
134
|
if (yield config.pagination(context)) {
|
|
116
135
|
currentPage++;
|
|
117
136
|
continue;
|
|
@@ -122,107 +141,54 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
122
141
|
});
|
|
123
142
|
return {
|
|
124
143
|
getHeaders: () => __awaiter(void 0, void 0, void 0, function* () { return Array.from((yield _getMap()).keys()); }),
|
|
125
|
-
|
|
126
|
-
const row = yield _findRowLocator(filters, options);
|
|
127
|
-
if (!row)
|
|
128
|
-
return resolve(config.rowSelector, rootLocator).filter({ hasText: "NON_EXISTENT_ROW_SENTINEL_" + Date.now() });
|
|
129
|
-
return row;
|
|
130
|
-
}),
|
|
131
|
-
getByCell: (rowFilters, targetColumn) => __awaiter(void 0, void 0, void 0, function* () {
|
|
132
|
-
const row = yield _findRowLocator(rowFilters);
|
|
133
|
-
if (!row)
|
|
134
|
-
throw new Error(`Row not found: ${JSON.stringify(rowFilters)}`);
|
|
135
|
-
// Guard: getByCell makes no sense for Menus (no cells)
|
|
136
|
-
if (!config.cellSelector) {
|
|
137
|
-
throw new Error("getByCell is not supported when 'cellSelector' is null (e.g. Menus). Use getByRow instead.");
|
|
138
|
-
}
|
|
144
|
+
getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
139
145
|
const map = yield _getMap();
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
throw new Error(`Column '${
|
|
143
|
-
|
|
144
|
-
return row.locator(config.cellSelector).nth(colIndex);
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
return resolve(config.cellSelector, row).nth(colIndex);
|
|
148
|
-
}
|
|
146
|
+
const idx = map.get(columnName);
|
|
147
|
+
if (idx === undefined)
|
|
148
|
+
throw new Error(`Column '${columnName}' not found.`);
|
|
149
|
+
return resolve(config.headerSelector, rootLocator).nth(idx);
|
|
149
150
|
}),
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
for (let i = 0; i < rowCount; i++) {
|
|
156
|
-
const row = rowLocator.nth(i);
|
|
157
|
-
let cellTexts = [];
|
|
158
|
-
if (!config.cellSelector) {
|
|
159
|
-
cellTexts = [yield row.innerText()];
|
|
160
|
-
}
|
|
161
|
-
else if (typeof config.cellSelector === 'string') {
|
|
162
|
-
// For string selectors, we query all matching cells in the row
|
|
163
|
-
cellTexts = yield row.locator(config.cellSelector).allInnerTexts();
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
// For function selectors, we resolve against the row
|
|
167
|
-
cellTexts = yield resolve(config.cellSelector, row).allInnerTexts();
|
|
168
|
-
}
|
|
169
|
-
const rowData = {};
|
|
170
|
-
for (const [colName, colIdx] of map.entries()) {
|
|
171
|
-
rowData[colName] = (cellTexts[colIdx] || "").trim();
|
|
172
|
-
}
|
|
173
|
-
results.push(rowData);
|
|
174
|
-
}
|
|
175
|
-
return results;
|
|
176
|
-
}),
|
|
177
|
-
getRowAsJSON: (filters) => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
|
-
const row = yield _findRowLocator(filters);
|
|
179
|
-
if (!row)
|
|
180
|
-
throw new Error(`Row not found: ${JSON.stringify(filters)}`);
|
|
181
|
-
let cellTexts = [];
|
|
182
|
-
if (!config.cellSelector) {
|
|
183
|
-
cellTexts = [yield row.innerText()];
|
|
151
|
+
getByRow: (filters, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
152
|
+
let row = yield _findRowLocator(filters, options);
|
|
153
|
+
// ā
FIX: Sentinel Logic for negative assertions (expect(row).not.toBeVisible())
|
|
154
|
+
if (!row) {
|
|
155
|
+
row = resolve(config.rowSelector, rootLocator).filter({ hasText: "___SENTINEL_ROW_NOT_FOUND___" + Date.now() });
|
|
184
156
|
}
|
|
185
|
-
|
|
186
|
-
|
|
157
|
+
const smartRow = _makeSmart(row, yield _getMap());
|
|
158
|
+
if (options === null || options === void 0 ? void 0 : options.asJSON) {
|
|
159
|
+
// If row doesn't exist, toJSON() returns empty object or throws?
|
|
160
|
+
// For safety, let's let it run naturally (it will likely return empty strings)
|
|
161
|
+
return smartRow.toJSON();
|
|
187
162
|
}
|
|
188
|
-
|
|
189
|
-
cellTexts = yield resolve(config.cellSelector, row).allInnerTexts();
|
|
190
|
-
}
|
|
191
|
-
const map = yield _getMap();
|
|
192
|
-
const result = {};
|
|
193
|
-
for (const [colName, colIndex] of map.entries()) {
|
|
194
|
-
result[colName] = (cellTexts[colIndex] || "").trim();
|
|
195
|
-
}
|
|
196
|
-
return result;
|
|
163
|
+
return smartRow;
|
|
197
164
|
}),
|
|
198
|
-
|
|
165
|
+
getAllRows: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
199
166
|
const map = yield _getMap();
|
|
200
|
-
let
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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));
|
|
175
|
+
if (options === null || options === void 0 ? void 0 : options.asJSON) {
|
|
176
|
+
return Promise.all(smartRows.map(r => r.toJSON()));
|
|
206
177
|
}
|
|
207
|
-
|
|
208
|
-
oldName = `__col_${colIndex}`;
|
|
209
|
-
const newName = typeof newNameOrFn === 'function' ? newNameOrFn(oldName) : newNameOrFn;
|
|
210
|
-
if (map.has(oldName))
|
|
211
|
-
map.delete(oldName);
|
|
212
|
-
map.set(newName, colIndex);
|
|
178
|
+
return smartRows;
|
|
213
179
|
}),
|
|
214
|
-
generateConfigPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
generateConfigPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
215
181
|
const html = yield rootLocator.evaluate((el) => el.outerHTML);
|
|
216
|
-
|
|
182
|
+
const separator = "=".repeat(50);
|
|
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);
|
|
217
185
|
}),
|
|
218
|
-
generateStrategyPrompt: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
186
|
+
generateStrategyPrompt: (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
187
|
const container = rootLocator.locator('xpath=..');
|
|
220
188
|
const html = yield container.evaluate((el) => el.outerHTML);
|
|
221
|
-
|
|
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);
|
|
222
191
|
})
|
|
223
192
|
};
|
|
224
193
|
};
|
|
225
194
|
exports.useTable = useTable;
|
|
226
|
-
function escapeRegExp(string) {
|
|
227
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
228
|
-
}
|
package/package.json
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rickcedwhat/playwright-smart-table",
|
|
3
|
-
"version": "
|
|
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
|
-
"
|
|
11
|
+
"generate-types": "node scripts/embed-types.mjs",
|
|
12
|
+
"build": "npm run generate-types && tsc",
|
|
12
13
|
"prepublishOnly": "npm run build",
|
|
13
|
-
"
|
|
14
|
+
"release": "npm run build && npm publish --access public",
|
|
14
15
|
"test": "npx playwright test"
|
|
15
16
|
},
|
|
16
|
-
"keywords": [
|
|
17
|
+
"keywords": [
|
|
18
|
+
"playwright",
|
|
19
|
+
"testing",
|
|
20
|
+
"table",
|
|
21
|
+
"automation"
|
|
22
|
+
],
|
|
17
23
|
"author": "",
|
|
18
24
|
"license": "ISC",
|
|
19
25
|
"peerDependencies": {
|
|
20
|
-
"@playwright/test": "*"
|
|
26
|
+
"@playwright/test": "*"
|
|
21
27
|
},
|
|
22
28
|
"peerDependenciesMeta": {
|
|
23
29
|
"@playwright/test": {
|