@rickcedwhat/playwright-smart-table 2.1.3 → 2.3.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 +151 -61
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/strategies/index.d.ts +2 -15
- package/dist/strategies/index.js +16 -109
- package/dist/strategies/pagination.d.ts +32 -0
- package/dist/strategies/pagination.js +72 -0
- package/dist/strategies/sorting.d.ts +12 -0
- package/dist/strategies/sorting.js +68 -0
- package/dist/typeContext.d.ts +1 -1
- package/dist/typeContext.js +67 -3
- package/dist/types.d.ts +61 -3
- package/dist/useTable.d.ts +37 -1
- package/dist/useTable.js +214 -9
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +29 -0
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.SortingStrategies = void 0;
|
|
13
|
+
/**
|
|
14
|
+
* A collection of pre-built sorting strategies.
|
|
15
|
+
*/
|
|
16
|
+
exports.SortingStrategies = {
|
|
17
|
+
/**
|
|
18
|
+
* A sorting strategy that interacts with column headers based on ARIA attributes.
|
|
19
|
+
* - `doSort`: Clicks the header repeatedly until the desired `aria-sort` state is achieved.
|
|
20
|
+
* - `getSortState`: Reads the `aria-sort` attribute from the header.
|
|
21
|
+
*/
|
|
22
|
+
AriaSort: () => {
|
|
23
|
+
return {
|
|
24
|
+
doSort(_a) {
|
|
25
|
+
return __awaiter(this, arguments, void 0, function* ({ columnName, direction, context }) {
|
|
26
|
+
const { resolve, config, root } = context;
|
|
27
|
+
const headerLoc = resolve(config.headerSelector, root);
|
|
28
|
+
const headers = yield headerLoc.all();
|
|
29
|
+
const headerTexts = yield Promise.all(headers.map(h => h.innerText()));
|
|
30
|
+
const columnIndex = headerTexts.findIndex(text => text.trim() === columnName);
|
|
31
|
+
if (columnIndex === -1) {
|
|
32
|
+
throw new Error(`[AriaSort] Header with text "${columnName}" not found.`);
|
|
33
|
+
}
|
|
34
|
+
const targetHeader = headers[columnIndex];
|
|
35
|
+
// Click repeatedly to cycle through sort states
|
|
36
|
+
for (let i = 0; i < 3; i++) { // Max 3 clicks to prevent infinite loops (none -> asc -> desc)
|
|
37
|
+
const currentState = yield targetHeader.getAttribute('aria-sort');
|
|
38
|
+
const mappedState = currentState === 'ascending' ? 'asc' : currentState === 'descending' ? 'desc' : 'none';
|
|
39
|
+
if (mappedState === direction) {
|
|
40
|
+
return; // Desired state achieved
|
|
41
|
+
}
|
|
42
|
+
yield targetHeader.click();
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`[AriaSort] Could not achieve sort direction "${direction}" for column "${columnName}" after 3 clicks.`);
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
getSortState(_a) {
|
|
48
|
+
return __awaiter(this, arguments, void 0, function* ({ columnName, context }) {
|
|
49
|
+
const { resolve, config, root } = context;
|
|
50
|
+
const headerLoc = resolve(config.headerSelector, root);
|
|
51
|
+
const headers = yield headerLoc.all();
|
|
52
|
+
const headerTexts = yield Promise.all(headers.map(h => h.innerText()));
|
|
53
|
+
const columnIndex = headerTexts.findIndex(text => text.trim() === columnName);
|
|
54
|
+
if (columnIndex === -1) {
|
|
55
|
+
return 'none'; // Header not found, so it's not sorted
|
|
56
|
+
}
|
|
57
|
+
const targetHeader = headers[columnIndex];
|
|
58
|
+
const ariaSort = yield targetHeader.getAttribute('aria-sort');
|
|
59
|
+
if (ariaSort === 'ascending')
|
|
60
|
+
return 'asc';
|
|
61
|
+
if (ariaSort === 'descending')
|
|
62
|
+
return 'desc';
|
|
63
|
+
return 'none';
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
package/dist/typeContext.d.ts
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* This file is generated by scripts/embed-types.js
|
|
4
4
|
* It contains the raw text of types.ts to provide context for LLM prompts.
|
|
5
5
|
*/
|
|
6
|
-
export declare const TYPE_CONTEXT = "\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:
|
|
6
|
+
export declare const TYPE_CONTEXT = "\nexport type Selector = string | ((root: Locator | Page) => Locator);\n\nexport type SmartRow = Omit<Locator, 'fill'> & {\n getCell(column: string): Locator;\n toJSON(): Promise<Record<string, string>>;\n /**\n * Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).\n */\n fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;\n};\n\nexport type StrategyContext = TableContext;\n\n/**\n * Defines the contract for a sorting strategy.\n */\nexport interface SortingStrategy {\n /**\n * Performs the sort action on a column.\n */\n doSort(options: {\n columnName: string;\n direction: 'asc' | 'desc';\n context: StrategyContext;\n }): Promise<void>;\n\n /**\n * Retrieves the current sort state of a column.\n */\n getSortState(options: {\n columnName: string;\n context: StrategyContext;\n }): Promise<'asc' | 'desc' | 'none'>;\n}\n\nexport interface TableContext {\n root: Locator;\n config: FinalTableConfig;\n page: Page;\n resolve: (selector: Selector, parent: Locator | Page) => Locator;\n}\n\nexport type PaginationStrategy = (context: TableContext) => Promise<boolean>;\n\nexport interface PromptOptions {\n /**\n * Output Strategy:\n * - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).\n * - 'console': Standard console logs (Default).\n */\n output?: 'console' | 'error';\n includeTypes?: boolean;\n}\n\nexport interface TableConfig {\n rowSelector?: Selector;\n headerSelector?: Selector;\n cellSelector?: Selector;\n pagination?: PaginationStrategy;\n sorting?: SortingStrategy;\n maxPages?: number;\n /**\n * Hook to rename columns dynamically.\n * * @param args.text - The default innerText of the header.\n * @param args.index - The column index.\n * @param args.locator - The specific header cell locator.\n */\n headerTransformer?: (args: { text: string, index: number, locator: Locator }) => string | Promise<string>;\n autoScroll?: boolean;\n /**\n * Enable debug mode to log internal state to console.\n */\n debug?: boolean;\n /**\n * Strategy to reset the table to the first page.\n * Called when table.reset() is invoked.\n */\n onReset?: (context: TableContext) => Promise<void>;\n}\n\n/**\n * Represents the final, resolved table configuration after default values have been applied.\n * All optional properties from TableConfig are now required, except for `sorting`.\n */\nexport type FinalTableConfig = Required<Omit<TableConfig, 'sorting'>> & {\n sorting?: SortingStrategy;\n};\n\nexport interface FillOptions {\n /**\n * Custom input mappers for specific columns.\n * Maps column names to functions that return the input locator for that cell.\n * Columns not specified here will use auto-detection.\n */\n inputMappers?: Record<string, (cell: Locator) => Locator>;\n}\n\nexport interface TableResult {\n getHeaders: () => Promise<string[]>;\n getHeaderCell: (columnName: string) => Promise<Locator>;\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 getAllRows: <T extends { asJSON?: boolean }>(\n options?: { filter?: Record<string, any>, exact?: boolean } & 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 /**\n * Resets the table state (clears cache, flags) and invokes the onReset strategy.\n */\n reset: () => Promise<void>;\n\n /**\n * Scans a specific column across all pages and returns the values.\n */\n getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;\n\n /**\n * Provides access to sorting actions and assertions.\n */\n sorting: {\n /**\n * Applies the configured sorting strategy to the specified column.\n * @param columnName The name of the column to sort.\n * @param direction The direction to sort ('asc' or 'desc').\n */\n apply(columnName: string, direction: 'asc' | 'desc'): Promise<void>;\n /**\n * Gets the current sort state of a column using the configured sorting strategy.\n * @param columnName The name of the column to check.\n * @returns A promise that resolves to 'asc', 'desc', or 'none'.\n */\n getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;\n };\n}\n";
|
package/dist/typeContext.js
CHANGED
|
@@ -9,14 +9,42 @@ exports.TYPE_CONTEXT = void 0;
|
|
|
9
9
|
exports.TYPE_CONTEXT = `
|
|
10
10
|
export type Selector = string | ((root: Locator | Page) => Locator);
|
|
11
11
|
|
|
12
|
-
export type SmartRow = Locator & {
|
|
12
|
+
export type SmartRow = Omit<Locator, 'fill'> & {
|
|
13
13
|
getCell(column: string): Locator;
|
|
14
14
|
toJSON(): Promise<Record<string, string>>;
|
|
15
|
+
/**
|
|
16
|
+
* Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
|
|
17
|
+
*/
|
|
18
|
+
fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
15
19
|
};
|
|
16
20
|
|
|
21
|
+
export type StrategyContext = TableContext;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Defines the contract for a sorting strategy.
|
|
25
|
+
*/
|
|
26
|
+
export interface SortingStrategy {
|
|
27
|
+
/**
|
|
28
|
+
* Performs the sort action on a column.
|
|
29
|
+
*/
|
|
30
|
+
doSort(options: {
|
|
31
|
+
columnName: string;
|
|
32
|
+
direction: 'asc' | 'desc';
|
|
33
|
+
context: StrategyContext;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Retrieves the current sort state of a column.
|
|
38
|
+
*/
|
|
39
|
+
getSortState(options: {
|
|
40
|
+
columnName: string;
|
|
41
|
+
context: StrategyContext;
|
|
42
|
+
}): Promise<'asc' | 'desc' | 'none'>;
|
|
43
|
+
}
|
|
44
|
+
|
|
17
45
|
export interface TableContext {
|
|
18
46
|
root: Locator;
|
|
19
|
-
config:
|
|
47
|
+
config: FinalTableConfig;
|
|
20
48
|
page: Page;
|
|
21
49
|
resolve: (selector: Selector, parent: Locator | Page) => Locator;
|
|
22
50
|
}
|
|
@@ -26,7 +54,7 @@ export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
|
26
54
|
export interface PromptOptions {
|
|
27
55
|
/**
|
|
28
56
|
* Output Strategy:
|
|
29
|
-
* - 'error': Throws an error with the prompt (
|
|
57
|
+
* - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).
|
|
30
58
|
* - 'console': Standard console logs (Default).
|
|
31
59
|
*/
|
|
32
60
|
output?: 'console' | 'error';
|
|
@@ -38,6 +66,7 @@ export interface TableConfig {
|
|
|
38
66
|
headerSelector?: Selector;
|
|
39
67
|
cellSelector?: Selector;
|
|
40
68
|
pagination?: PaginationStrategy;
|
|
69
|
+
sorting?: SortingStrategy;
|
|
41
70
|
maxPages?: number;
|
|
42
71
|
/**
|
|
43
72
|
* Hook to rename columns dynamically.
|
|
@@ -58,6 +87,23 @@ export interface TableConfig {
|
|
|
58
87
|
onReset?: (context: TableContext) => Promise<void>;
|
|
59
88
|
}
|
|
60
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Represents the final, resolved table configuration after default values have been applied.
|
|
92
|
+
* All optional properties from TableConfig are now required, except for \`sorting\`.
|
|
93
|
+
*/
|
|
94
|
+
export type FinalTableConfig = Required<Omit<TableConfig, 'sorting'>> & {
|
|
95
|
+
sorting?: SortingStrategy;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export interface FillOptions {
|
|
99
|
+
/**
|
|
100
|
+
* Custom input mappers for specific columns.
|
|
101
|
+
* Maps column names to functions that return the input locator for that cell.
|
|
102
|
+
* Columns not specified here will use auto-detection.
|
|
103
|
+
*/
|
|
104
|
+
inputMappers?: Record<string, (cell: Locator) => Locator>;
|
|
105
|
+
}
|
|
106
|
+
|
|
61
107
|
export interface TableResult {
|
|
62
108
|
getHeaders: () => Promise<string[]>;
|
|
63
109
|
getHeaderCell: (columnName: string) => Promise<Locator>;
|
|
@@ -83,5 +129,23 @@ export interface TableResult {
|
|
|
83
129
|
* Scans a specific column across all pages and returns the values.
|
|
84
130
|
*/
|
|
85
131
|
getColumnValues: <V = string>(column: string, options?: { mapper?: (cell: Locator) => Promise<V> | V, maxPages?: number }) => Promise<V[]>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Provides access to sorting actions and assertions.
|
|
135
|
+
*/
|
|
136
|
+
sorting: {
|
|
137
|
+
/**
|
|
138
|
+
* Applies the configured sorting strategy to the specified column.
|
|
139
|
+
* @param columnName The name of the column to sort.
|
|
140
|
+
* @param direction The direction to sort ('asc' or 'desc').
|
|
141
|
+
*/
|
|
142
|
+
apply(columnName: string, direction: 'asc' | 'desc'): Promise<void>;
|
|
143
|
+
/**
|
|
144
|
+
* Gets the current sort state of a column using the configured sorting strategy.
|
|
145
|
+
* @param columnName The name of the column to check.
|
|
146
|
+
* @returns A promise that resolves to 'asc', 'desc', or 'none'.
|
|
147
|
+
*/
|
|
148
|
+
getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;
|
|
149
|
+
};
|
|
86
150
|
}
|
|
87
151
|
`;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import type { Locator, Page } from '@playwright/test';
|
|
2
2
|
export type Selector = string | ((root: Locator | Page) => Locator);
|
|
3
|
-
export type SmartRow = Locator & {
|
|
3
|
+
export type SmartRow = Omit<Locator, 'fill'> & {
|
|
4
4
|
getCell(column: string): Locator;
|
|
5
5
|
toJSON(): Promise<Record<string, string>>;
|
|
6
|
+
/**
|
|
7
|
+
* Fills the row with data. Automatically detects input types (text input, select, checkbox, etc.).
|
|
8
|
+
*/
|
|
9
|
+
fill: (data: Record<string, any>, options?: FillOptions) => Promise<void>;
|
|
6
10
|
};
|
|
11
|
+
export type StrategyContext = TableContext;
|
|
12
|
+
/**
|
|
13
|
+
* Defines the contract for a sorting strategy.
|
|
14
|
+
*/
|
|
15
|
+
export interface SortingStrategy {
|
|
16
|
+
/**
|
|
17
|
+
* Performs the sort action on a column.
|
|
18
|
+
*/
|
|
19
|
+
doSort(options: {
|
|
20
|
+
columnName: string;
|
|
21
|
+
direction: 'asc' | 'desc';
|
|
22
|
+
context: StrategyContext;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves the current sort state of a column.
|
|
26
|
+
*/
|
|
27
|
+
getSortState(options: {
|
|
28
|
+
columnName: string;
|
|
29
|
+
context: StrategyContext;
|
|
30
|
+
}): Promise<'asc' | 'desc' | 'none'>;
|
|
31
|
+
}
|
|
7
32
|
export interface TableContext {
|
|
8
33
|
root: Locator;
|
|
9
|
-
config:
|
|
34
|
+
config: FinalTableConfig;
|
|
10
35
|
page: Page;
|
|
11
36
|
resolve: (selector: Selector, parent: Locator | Page) => Locator;
|
|
12
37
|
}
|
|
@@ -14,7 +39,7 @@ export type PaginationStrategy = (context: TableContext) => Promise<boolean>;
|
|
|
14
39
|
export interface PromptOptions {
|
|
15
40
|
/**
|
|
16
41
|
* Output Strategy:
|
|
17
|
-
* - 'error': Throws an error with the prompt (
|
|
42
|
+
* - 'error': Throws an error with the prompt (useful for platforms that capture error output cleanly).
|
|
18
43
|
* - 'console': Standard console logs (Default).
|
|
19
44
|
*/
|
|
20
45
|
output?: 'console' | 'error';
|
|
@@ -25,6 +50,7 @@ export interface TableConfig {
|
|
|
25
50
|
headerSelector?: Selector;
|
|
26
51
|
cellSelector?: Selector;
|
|
27
52
|
pagination?: PaginationStrategy;
|
|
53
|
+
sorting?: SortingStrategy;
|
|
28
54
|
maxPages?: number;
|
|
29
55
|
/**
|
|
30
56
|
* Hook to rename columns dynamically.
|
|
@@ -48,6 +74,21 @@ export interface TableConfig {
|
|
|
48
74
|
*/
|
|
49
75
|
onReset?: (context: TableContext) => Promise<void>;
|
|
50
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Represents the final, resolved table configuration after default values have been applied.
|
|
79
|
+
* All optional properties from TableConfig are now required, except for `sorting`.
|
|
80
|
+
*/
|
|
81
|
+
export type FinalTableConfig = Required<Omit<TableConfig, 'sorting'>> & {
|
|
82
|
+
sorting?: SortingStrategy;
|
|
83
|
+
};
|
|
84
|
+
export interface FillOptions {
|
|
85
|
+
/**
|
|
86
|
+
* Custom input mappers for specific columns.
|
|
87
|
+
* Maps column names to functions that return the input locator for that cell.
|
|
88
|
+
* Columns not specified here will use auto-detection.
|
|
89
|
+
*/
|
|
90
|
+
inputMappers?: Record<string, (cell: Locator) => Locator>;
|
|
91
|
+
}
|
|
51
92
|
export interface TableResult {
|
|
52
93
|
getHeaders: () => Promise<string[]>;
|
|
53
94
|
getHeaderCell: (columnName: string) => Promise<Locator>;
|
|
@@ -76,4 +117,21 @@ export interface TableResult {
|
|
|
76
117
|
mapper?: (cell: Locator) => Promise<V> | V;
|
|
77
118
|
maxPages?: number;
|
|
78
119
|
}) => Promise<V[]>;
|
|
120
|
+
/**
|
|
121
|
+
* Provides access to sorting actions and assertions.
|
|
122
|
+
*/
|
|
123
|
+
sorting: {
|
|
124
|
+
/**
|
|
125
|
+
* Applies the configured sorting strategy to the specified column.
|
|
126
|
+
* @param columnName The name of the column to sort.
|
|
127
|
+
* @param direction The direction to sort ('asc' or 'desc').
|
|
128
|
+
*/
|
|
129
|
+
apply(columnName: string, direction: 'asc' | 'desc'): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Gets the current sort state of a column using the configured sorting strategy.
|
|
132
|
+
* @param columnName The name of the column to check.
|
|
133
|
+
* @returns A promise that resolves to 'asc', 'desc', or 'none'.
|
|
134
|
+
*/
|
|
135
|
+
getState(columnName: string): Promise<'asc' | 'desc' | 'none'>;
|
|
136
|
+
};
|
|
79
137
|
}
|
package/dist/useTable.d.ts
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
1
|
import type { Locator } from '@playwright/test';
|
|
2
|
-
import { TableConfig, TableResult } from './types';
|
|
2
|
+
import { TableConfig, TableContext, TableResult } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* A collection of pre-built pagination strategies.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PaginationStrategies: {
|
|
7
|
+
/**
|
|
8
|
+
* Clicks a "Next" button.
|
|
9
|
+
* @param selector - The CSS selector for the "Next" button.
|
|
10
|
+
*/
|
|
11
|
+
NextButton: (selector: string) => ((context: TableContext) => Promise<boolean>);
|
|
12
|
+
/**
|
|
13
|
+
* Clicks numbered page links.
|
|
14
|
+
* @param selector - The CSS selector for the page number links.
|
|
15
|
+
*/
|
|
16
|
+
NumberedPages: (selector: string) => ((context: TableContext) => Promise<boolean>);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* @deprecated Use `PaginationStrategies` instead. This alias will be removed in a future major version.
|
|
20
|
+
*/
|
|
21
|
+
export declare const TableStrategies: {
|
|
22
|
+
/**
|
|
23
|
+
* Clicks a "Next" button.
|
|
24
|
+
* @param selector - The CSS selector for the "Next" button.
|
|
25
|
+
*/
|
|
26
|
+
NextButton: (selector: string) => ((context: TableContext) => Promise<boolean>);
|
|
27
|
+
/**
|
|
28
|
+
* Clicks numbered page links.
|
|
29
|
+
* @param selector - The CSS selector for the page number links.
|
|
30
|
+
*/
|
|
31
|
+
NumberedPages: (selector: string) => ((context: TableContext) => Promise<boolean>);
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A collection of pre-built sorting strategies.
|
|
35
|
+
*/
|
|
36
|
+
export declare const SortingStrategies: {
|
|
37
|
+
AriaSort: () => import("./types").SortingStrategy;
|
|
38
|
+
};
|
|
3
39
|
export declare const useTable: (rootLocator: Locator, configOptions?: TableConfig) => TableResult;
|
package/dist/useTable.js
CHANGED
|
@@ -9,8 +9,52 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.useTable = void 0;
|
|
12
|
+
exports.useTable = exports.SortingStrategies = exports.TableStrategies = exports.PaginationStrategies = void 0;
|
|
13
13
|
const typeContext_1 = require("./typeContext");
|
|
14
|
+
const sorting_1 = require("./strategies/sorting");
|
|
15
|
+
/**
|
|
16
|
+
* A collection of pre-built pagination strategies.
|
|
17
|
+
*/
|
|
18
|
+
exports.PaginationStrategies = {
|
|
19
|
+
/**
|
|
20
|
+
* Clicks a "Next" button.
|
|
21
|
+
* @param selector - The CSS selector for the "Next" button.
|
|
22
|
+
*/
|
|
23
|
+
NextButton: (selector) => {
|
|
24
|
+
return (_a) => __awaiter(void 0, [_a], void 0, function* ({ root }) {
|
|
25
|
+
const nextButton = root.locator(selector);
|
|
26
|
+
if ((yield nextButton.isVisible()) && (yield nextButton.isEnabled())) {
|
|
27
|
+
yield nextButton.click();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
/**
|
|
34
|
+
* Clicks numbered page links.
|
|
35
|
+
* @param selector - The CSS selector for the page number links.
|
|
36
|
+
*/
|
|
37
|
+
NumberedPages: (selector) => {
|
|
38
|
+
let currentPage = 1;
|
|
39
|
+
return (_a) => __awaiter(void 0, [_a], void 0, function* ({ root }) {
|
|
40
|
+
currentPage++;
|
|
41
|
+
const pageLink = root.locator(selector).filter({ hasText: String(currentPage) });
|
|
42
|
+
if (yield pageLink.isVisible()) {
|
|
43
|
+
yield pageLink.click();
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* @deprecated Use `PaginationStrategies` instead. This alias will be removed in a future major version.
|
|
52
|
+
*/
|
|
53
|
+
exports.TableStrategies = exports.PaginationStrategies;
|
|
54
|
+
/**
|
|
55
|
+
* A collection of pre-built sorting strategies.
|
|
56
|
+
*/
|
|
57
|
+
exports.SortingStrategies = sorting_1.SortingStrategies;
|
|
14
58
|
const useTable = (rootLocator, configOptions = {}) => {
|
|
15
59
|
const config = Object.assign({ rowSelector: "tbody tr", headerSelector: "th", cellSelector: "td", pagination: () => __awaiter(void 0, void 0, void 0, function* () { return false; }), maxPages: 1, headerTransformer: ({ text, index, locator }) => text, autoScroll: true, debug: false, onReset: () => __awaiter(void 0, void 0, void 0, function* () { console.warn("⚠️ .reset() called but no 'onReset' strategy defined in config."); }) }, configOptions);
|
|
16
60
|
const resolve = (item, parent) => {
|
|
@@ -27,6 +71,30 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
27
71
|
if (config.debug)
|
|
28
72
|
console.log(`🔎 [SmartTable Debug] ${msg}`);
|
|
29
73
|
};
|
|
74
|
+
const _suggestColumnName = (colName, availableColumns) => {
|
|
75
|
+
// Simple fuzzy matching - find columns with similar names
|
|
76
|
+
const lowerCol = colName.toLowerCase();
|
|
77
|
+
const suggestions = availableColumns.filter(col => col.toLowerCase().includes(lowerCol) ||
|
|
78
|
+
lowerCol.includes(col.toLowerCase()) ||
|
|
79
|
+
col.toLowerCase().replace(/\s+/g, '') === lowerCol.replace(/\s+/g, ''));
|
|
80
|
+
if (suggestions.length > 0 && suggestions[0] !== colName) {
|
|
81
|
+
return `. Did you mean "${suggestions[0]}"?`;
|
|
82
|
+
}
|
|
83
|
+
// Show similar column names (first 3)
|
|
84
|
+
if (availableColumns.length > 0 && availableColumns.length <= 10) {
|
|
85
|
+
return `. Available columns: ${availableColumns.map(c => `"${c}"`).join(', ')}`;
|
|
86
|
+
}
|
|
87
|
+
else if (availableColumns.length > 0) {
|
|
88
|
+
return `. Available columns (first 5): ${availableColumns.slice(0, 5).map(c => `"${c}"`).join(', ')}, ...`;
|
|
89
|
+
}
|
|
90
|
+
return '.';
|
|
91
|
+
};
|
|
92
|
+
const _createColumnError = (colName, map, context) => {
|
|
93
|
+
const availableColumns = Array.from(map.keys());
|
|
94
|
+
const suggestion = _suggestColumnName(colName, availableColumns);
|
|
95
|
+
const contextMsg = context ? ` (${context})` : '';
|
|
96
|
+
return new Error(`Column "${colName}" not found${contextMsg}${suggestion}`);
|
|
97
|
+
};
|
|
30
98
|
const _getMap = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
31
99
|
if (_headerMap)
|
|
32
100
|
return _headerMap;
|
|
@@ -65,8 +133,11 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
65
133
|
const smart = rowLocator;
|
|
66
134
|
smart.getCell = (colName) => {
|
|
67
135
|
const idx = map.get(colName);
|
|
68
|
-
if (idx === undefined)
|
|
69
|
-
|
|
136
|
+
if (idx === undefined) {
|
|
137
|
+
const availableColumns = Array.from(map.keys());
|
|
138
|
+
const suggestion = _suggestColumnName(colName, availableColumns);
|
|
139
|
+
throw new Error(`Column "${colName}" not found${suggestion}`);
|
|
140
|
+
}
|
|
70
141
|
if (typeof config.cellSelector === 'string') {
|
|
71
142
|
return rowLocator.locator(config.cellSelector).nth(idx);
|
|
72
143
|
}
|
|
@@ -85,6 +156,92 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
85
156
|
}
|
|
86
157
|
return result;
|
|
87
158
|
});
|
|
159
|
+
smart.fill = (data, fillOptions) => __awaiter(void 0, void 0, void 0, function* () {
|
|
160
|
+
var _a;
|
|
161
|
+
logDebug(`Filling row with data: ${JSON.stringify(data)}`);
|
|
162
|
+
// Fill each column
|
|
163
|
+
for (const [colName, value] of Object.entries(data)) {
|
|
164
|
+
const colIdx = map.get(colName);
|
|
165
|
+
if (colIdx === undefined) {
|
|
166
|
+
throw _createColumnError(colName, map, 'in fill data');
|
|
167
|
+
}
|
|
168
|
+
const cell = smart.getCell(colName);
|
|
169
|
+
// Use custom input mapper for this column if provided, otherwise auto-detect
|
|
170
|
+
let inputLocator;
|
|
171
|
+
if ((_a = fillOptions === null || fillOptions === void 0 ? void 0 : fillOptions.inputMappers) === null || _a === void 0 ? void 0 : _a[colName]) {
|
|
172
|
+
inputLocator = fillOptions.inputMappers[colName](cell);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Auto-detect input type
|
|
176
|
+
// Try different input types in order of commonality
|
|
177
|
+
// Check for text input
|
|
178
|
+
const textInput = cell.locator('input[type="text"], input:not([type]), textarea').first();
|
|
179
|
+
const textInputCount = yield textInput.count().catch(() => 0);
|
|
180
|
+
// Check for select
|
|
181
|
+
const select = cell.locator('select').first();
|
|
182
|
+
const selectCount = yield select.count().catch(() => 0);
|
|
183
|
+
// Check for checkbox/radio
|
|
184
|
+
const checkbox = cell.locator('input[type="checkbox"], input[type="radio"], [role="checkbox"]').first();
|
|
185
|
+
const checkboxCount = yield checkbox.count().catch(() => 0);
|
|
186
|
+
// Check for contenteditable or div-based inputs
|
|
187
|
+
const contentEditable = cell.locator('[contenteditable="true"]').first();
|
|
188
|
+
const contentEditableCount = yield contentEditable.count().catch(() => 0);
|
|
189
|
+
// Determine which input to use (prioritize by commonality)
|
|
190
|
+
if (textInputCount > 0 && selectCount === 0 && checkboxCount === 0) {
|
|
191
|
+
inputLocator = textInput;
|
|
192
|
+
}
|
|
193
|
+
else if (selectCount > 0) {
|
|
194
|
+
inputLocator = select;
|
|
195
|
+
}
|
|
196
|
+
else if (checkboxCount > 0) {
|
|
197
|
+
inputLocator = checkbox;
|
|
198
|
+
}
|
|
199
|
+
else if (contentEditableCount > 0) {
|
|
200
|
+
inputLocator = contentEditable;
|
|
201
|
+
}
|
|
202
|
+
else if (textInputCount > 0) {
|
|
203
|
+
// Fallback to text input even if others exist
|
|
204
|
+
inputLocator = textInput;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// No input found - try to click the cell itself (might trigger an editor)
|
|
208
|
+
inputLocator = cell;
|
|
209
|
+
}
|
|
210
|
+
// Warn if multiple inputs found (ambiguous)
|
|
211
|
+
const totalInputs = textInputCount + selectCount + checkboxCount + contentEditableCount;
|
|
212
|
+
if (totalInputs > 1 && config.debug) {
|
|
213
|
+
logDebug(`⚠️ Multiple inputs found in cell "${colName}" (${totalInputs} total). Using first match. Consider using inputMapper option for explicit control.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Fill based on value type and input type
|
|
217
|
+
const inputTag = yield inputLocator.evaluate((el) => el.tagName.toLowerCase()).catch(() => 'unknown');
|
|
218
|
+
const inputType = yield inputLocator.getAttribute('type').catch(() => null);
|
|
219
|
+
const isContentEditable = yield inputLocator.getAttribute('contenteditable').catch(() => null);
|
|
220
|
+
logDebug(`Filling "${colName}" with value "${value}" (input: ${inputTag}, type: ${inputType})`);
|
|
221
|
+
if (inputType === 'checkbox' || inputType === 'radio') {
|
|
222
|
+
// Boolean value for checkbox/radio
|
|
223
|
+
const shouldBeChecked = Boolean(value);
|
|
224
|
+
const isChecked = yield inputLocator.isChecked().catch(() => false);
|
|
225
|
+
if (isChecked !== shouldBeChecked) {
|
|
226
|
+
yield inputLocator.click();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (inputTag === 'select') {
|
|
230
|
+
// Select dropdown
|
|
231
|
+
yield inputLocator.selectOption(String(value));
|
|
232
|
+
}
|
|
233
|
+
else if (isContentEditable === 'true') {
|
|
234
|
+
// Contenteditable div
|
|
235
|
+
yield inputLocator.click();
|
|
236
|
+
yield inputLocator.fill(String(value));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
// Text input, textarea, or generic
|
|
240
|
+
yield inputLocator.fill(String(value));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
logDebug('Fill operation completed');
|
|
244
|
+
});
|
|
88
245
|
return smart;
|
|
89
246
|
};
|
|
90
247
|
const _applyFilters = (baseRows, filters, map, exact) => {
|
|
@@ -92,8 +249,9 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
92
249
|
const page = rootLocator.page();
|
|
93
250
|
for (const [colName, value] of Object.entries(filters)) {
|
|
94
251
|
const colIndex = map.get(colName);
|
|
95
|
-
if (colIndex === undefined)
|
|
96
|
-
throw
|
|
252
|
+
if (colIndex === undefined) {
|
|
253
|
+
throw _createColumnError(colName, map, 'in filter');
|
|
254
|
+
}
|
|
97
255
|
const filterVal = typeof value === 'number' ? String(value) : value;
|
|
98
256
|
const cellTemplate = resolve(config.cellSelector, page);
|
|
99
257
|
filtered = filtered.filter({
|
|
@@ -113,8 +271,26 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
113
271
|
const matchedRows = _applyFilters(allRows, filters, map, options.exact || false);
|
|
114
272
|
const count = yield matchedRows.count();
|
|
115
273
|
logDebug(`Page ${currentPage}: Found ${count} matches.`);
|
|
116
|
-
if (count > 1)
|
|
117
|
-
|
|
274
|
+
if (count > 1) {
|
|
275
|
+
// Try to get sample row data to help user identify the issue
|
|
276
|
+
const sampleData = [];
|
|
277
|
+
try {
|
|
278
|
+
const firstFewRows = yield matchedRows.all();
|
|
279
|
+
const sampleCount = Math.min(firstFewRows.length, 3);
|
|
280
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
281
|
+
const rowData = yield _makeSmart(firstFewRows[i], map).toJSON();
|
|
282
|
+
sampleData.push(JSON.stringify(rowData));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
// If we can't extract sample data, that's okay - continue without it
|
|
287
|
+
}
|
|
288
|
+
const sampleMsg = sampleData.length > 0
|
|
289
|
+
? `\nSample matching rows:\n${sampleData.map((d, i) => ` ${i + 1}. ${d}`).join('\n')}`
|
|
290
|
+
: '';
|
|
291
|
+
throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)} on page ${currentPage}. ` +
|
|
292
|
+
`Expected exactly one match. Try adding more filters to make your query unique.${sampleMsg}`);
|
|
293
|
+
}
|
|
118
294
|
if (count === 1)
|
|
119
295
|
return matchedRows.first();
|
|
120
296
|
if (currentPage < effectiveMaxPages) {
|
|
@@ -181,13 +357,41 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
181
357
|
return clone.outerHTML;
|
|
182
358
|
});
|
|
183
359
|
});
|
|
360
|
+
const sortingNamespace = {
|
|
361
|
+
apply: (columnName, direction) => __awaiter(void 0, void 0, void 0, function* () {
|
|
362
|
+
if (!config.sorting) {
|
|
363
|
+
throw new Error('No sorting strategy has been configured. Please add a `sorting` strategy to your useTable config.');
|
|
364
|
+
}
|
|
365
|
+
logDebug(`Applying sort for column "${columnName}" (${direction})`);
|
|
366
|
+
const context = {
|
|
367
|
+
root: rootLocator,
|
|
368
|
+
config: config,
|
|
369
|
+
page: rootLocator.page(),
|
|
370
|
+
resolve: resolve
|
|
371
|
+
};
|
|
372
|
+
yield config.sorting.doSort({ columnName, direction, context });
|
|
373
|
+
}),
|
|
374
|
+
getState: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
375
|
+
if (!config.sorting) {
|
|
376
|
+
throw new Error('No sorting strategy has been configured. Please add a `sorting` strategy to your useTable config.');
|
|
377
|
+
}
|
|
378
|
+
logDebug(`Getting sort state for column "${columnName}"`);
|
|
379
|
+
const context = {
|
|
380
|
+
root: rootLocator,
|
|
381
|
+
config: config,
|
|
382
|
+
page: rootLocator.page(),
|
|
383
|
+
resolve: resolve
|
|
384
|
+
};
|
|
385
|
+
return config.sorting.getSortState({ columnName, context });
|
|
386
|
+
})
|
|
387
|
+
};
|
|
184
388
|
return {
|
|
185
389
|
getHeaders: () => __awaiter(void 0, void 0, void 0, function* () { return Array.from((yield _getMap()).keys()); }),
|
|
186
390
|
getHeaderCell: (columnName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
187
391
|
const map = yield _getMap();
|
|
188
392
|
const idx = map.get(columnName);
|
|
189
393
|
if (idx === undefined)
|
|
190
|
-
throw
|
|
394
|
+
throw _createColumnError(columnName, map, 'header cell');
|
|
191
395
|
return resolve(config.headerSelector, rootLocator).nth(idx);
|
|
192
396
|
}),
|
|
193
397
|
reset: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -208,7 +412,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
208
412
|
const map = yield _getMap();
|
|
209
413
|
const colIdx = map.get(column);
|
|
210
414
|
if (colIdx === undefined)
|
|
211
|
-
throw
|
|
415
|
+
throw _createColumnError(column, map);
|
|
212
416
|
const mapper = (_a = options === null || options === void 0 ? void 0 : options.mapper) !== null && _a !== void 0 ? _a : ((c) => c.innerText());
|
|
213
417
|
const effectiveMaxPages = (_b = options === null || options === void 0 ? void 0 : options.maxPages) !== null && _b !== void 0 ? _b : config.maxPages;
|
|
214
418
|
let currentPage = 1;
|
|
@@ -272,6 +476,7 @@ const useTable = (rootLocator, configOptions = {}) => {
|
|
|
272
476
|
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, 10000)} ...\n\`\`\`\n`;
|
|
273
477
|
yield _handlePrompt('Smart Table Strategy', content, options);
|
|
274
478
|
}),
|
|
479
|
+
sorting: sortingNamespace,
|
|
275
480
|
};
|
|
276
481
|
};
|
|
277
482
|
exports.useTable = useTable;
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
|
+
/**
|
|
3
|
+
* Internal helper to wait for a condition to be met.
|
|
4
|
+
* Replaces the dependency on 'expect(...).toPass()' to ensure compatibility
|
|
5
|
+
* with environments where 'expect' is not globally available.
|
|
6
|
+
*/
|
|
7
|
+
export declare const waitForCondition: (predicate: () => Promise<boolean>, timeout: number, page: Page) => Promise<boolean>;
|