@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.
@@ -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
+ };
@@ -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: Required<TableConfig>;\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 (Best for Cloud/QA Wolf to get clean text).\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 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\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";
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";
@@ -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: Required<TableConfig>;
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 (Best for Cloud/QA Wolf to get clean text).
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: Required<TableConfig>;
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 (Best for Cloud/QA Wolf to get clean text).
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
  }
@@ -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
- throw new Error(`Column '${colName}' not found.`);
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 new Error(`Column '${colName}' not found.`);
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
- throw new Error(`Strict Mode Violation: Found ${count} rows matching ${JSON.stringify(filters)}.`);
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 new Error(`Column '${columnName}' not found.`);
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 new Error(`Column '${column}' not found.`);
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;
@@ -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>;