@thinkwise/testwise 0.2.7 → 0.2.20

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.
Files changed (60) hide show
  1. package/artifact-builder/InterfaceGenerator.ts +2 -2
  2. package/artifact-builder/SchemaGenerator.ts +2 -2
  3. package/artifact-builder/SelectorBuilder.ts +6 -2
  4. package/artifact-builder/SubjectComponentGenerator.ts +68 -10
  5. package/artifact-builder/SubjectGenerator.ts +2 -2
  6. package/artifact-builder/SubjectRegistration.ts +14 -0
  7. package/artifact-builder/helpers/NamingHandler.ts +28 -56
  8. package/components/export/ExportComponent.ts +1 -0
  9. package/components/tab/BaseTabObjects.ts +1 -1
  10. package/controls/DateControl.ts +67 -0
  11. package/controls/FormComboBox.ts +74 -0
  12. package/controls/InputFieldControl.ts +79 -0
  13. package/controls/LookupDropdown.ts +4 -2
  14. package/controls/index.ts +3 -0
  15. package/dist/artifact-builder/InterfaceGenerator.js +3 -3
  16. package/dist/artifact-builder/InterfaceGenerator.js.map +1 -1
  17. package/dist/artifact-builder/SchemaGenerator.js +2 -2
  18. package/dist/artifact-builder/SchemaGenerator.js.map +1 -1
  19. package/dist/artifact-builder/SelectorBuilder.d.ts +1 -0
  20. package/dist/artifact-builder/SelectorBuilder.js +4 -1
  21. package/dist/artifact-builder/SelectorBuilder.js.map +1 -1
  22. package/dist/artifact-builder/SubjectComponentGenerator.d.ts +2 -2
  23. package/dist/artifact-builder/SubjectComponentGenerator.js +50 -11
  24. package/dist/artifact-builder/SubjectComponentGenerator.js.map +1 -1
  25. package/dist/artifact-builder/SubjectGenerator.js +2 -2
  26. package/dist/artifact-builder/SubjectGenerator.js.map +1 -1
  27. package/dist/artifact-builder/SubjectRegistration.d.ts +1 -0
  28. package/dist/artifact-builder/SubjectRegistration.js +11 -0
  29. package/dist/artifact-builder/SubjectRegistration.js.map +1 -1
  30. package/dist/artifact-builder/helpers/NamingHandler.d.ts +2 -4
  31. package/dist/artifact-builder/helpers/NamingHandler.js +26 -52
  32. package/dist/artifact-builder/helpers/NamingHandler.js.map +1 -1
  33. package/dist/components/export/ExportComponent.js +1 -0
  34. package/dist/components/export/ExportComponent.js.map +1 -1
  35. package/dist/components/tab/BaseTabObjects.js +1 -1
  36. package/dist/components/tab/BaseTabObjects.js.map +1 -1
  37. package/dist/controls/DateControl.d.ts +25 -0
  38. package/dist/controls/DateControl.js +44 -0
  39. package/dist/controls/DateControl.js.map +1 -0
  40. package/dist/controls/FormComboBox.d.ts +9 -0
  41. package/dist/controls/FormComboBox.js +45 -0
  42. package/dist/controls/FormComboBox.js.map +1 -0
  43. package/dist/controls/InputFieldControl.d.ts +26 -0
  44. package/dist/controls/InputFieldControl.js +48 -0
  45. package/dist/controls/InputFieldControl.js.map +1 -0
  46. package/dist/controls/LookupDropdown.d.ts +2 -1
  47. package/dist/controls/LookupDropdown.js.map +1 -1
  48. package/dist/controls/index.d.ts +3 -0
  49. package/dist/controls/index.js +3 -0
  50. package/dist/controls/index.js.map +1 -1
  51. package/dist/enums/ElementTypes.d.ts +4 -2
  52. package/dist/enums/ElementTypes.js +3 -1
  53. package/dist/enums/ElementTypes.js.map +1 -1
  54. package/dist/helpers/UserSimulationHelper.js +1 -1
  55. package/dist/helpers/UserSimulationHelper.js.map +1 -1
  56. package/enums/ElementTypes.ts +4 -2
  57. package/helpers/UserSimulationHelper.ts +1 -1
  58. package/package.json +7 -7
  59. package/scripts/postinstall.js +3 -1
  60. package/scripts/sync.js +748 -756
@@ -77,7 +77,7 @@ export class InterfaceGenerator {
77
77
  const baseFileName = file.slice(0, file.lastIndexOf('.'));
78
78
  const correctInterfaceName = isInterface
79
79
  ? this._namingHandler.generateScreenInterfaceName(baseFileName)
80
- : this._namingHandler.snakeToPascalCase(baseFileName);
80
+ : this._namingHandler.toPascalCase(baseFileName);
81
81
 
82
82
  const schemaPath = path.join(schemasDirectory, file);
83
83
  const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
@@ -139,7 +139,7 @@ export class InterfaceGenerator {
139
139
  }
140
140
 
141
141
  if (schemaType === SchemaType.Screen) {
142
- const fileNameSnakeCase = this._namingHandler.pascalToSnakeCase(fileName);
142
+ const fileNameSnakeCase = this._namingHandler.toSnakeCase(fileName);
143
143
 
144
144
  if (this._screensToBuild.length > 0 && !this._screensToBuild.includes(fileNameSnakeCase)) {
145
145
  return Promise.resolve(true);
@@ -75,7 +75,7 @@ export class SchemaGenerator {
75
75
  }
76
76
 
77
77
  private getScreenContent(screenTypeId: string): string {
78
- const screenFileName = this._namingHandler.formatPascalCaseName(screenTypeId);
78
+ const screenFileName = this._namingHandler.toPascalCase(screenTypeId);
79
79
  const _filename = fileURLToPath(import.meta.url);
80
80
  const _dirname = path.dirname(_filename);
81
81
  const screenLocation = path.resolve(_dirname, '..', '..', 'test-artifacts/screens/');
@@ -86,7 +86,7 @@ export class SchemaGenerator {
86
86
  private createComponentSchema(subject: ISubject, componentType: SubjectComponents): void {
87
87
  const interfaceName = `${subject.subject}_${componentType.toLowerCase()}`;
88
88
  const schemasDir = path.join(currentDirname, '..', '..', 'dist/test-artifacts/schemas');
89
- const fileName = `${this._namingHandler.snakeToPascalCase(interfaceName)}.json`;
89
+ const fileName = `${this._namingHandler.toPascalCase(interfaceName)}.json`;
90
90
 
91
91
  if (fs.existsSync(path.join(schemasDir, fileName))) {
92
92
  return;
@@ -52,12 +52,16 @@ export class SelectorBuilder {
52
52
  return suffix;
53
53
  }
54
54
 
55
- public getPageLocatorString(property: string, componentName: SubjectComponents): string {
56
- let elementId = this._namingHandler.getIdFromElementName(
55
+ public getElementId(property: string, componentName: SubjectComponents): string {
56
+ return this._namingHandler.getIdFromElementName(
57
57
  property,
58
58
  componentName,
59
59
  this._namingHandler.getElementTypeFromElementName(property)
60
60
  );
61
+ }
62
+
63
+ public getPageLocatorString(property: string, componentName: SubjectComponents): string {
64
+ let elementId = this.getElementId(property, componentName);
61
65
 
62
66
  if (componentName === 'Grid') elementId = elementId.replace(/-/g, '_');
63
67
 
@@ -105,9 +105,13 @@ export class SubjectComponentGenerator {
105
105
  gridGetters = this.generateGridGetters(propertyLines, componentName);
106
106
  } else {
107
107
  propertyDeclarations = this.generatePropertyDeclarations(propertyLines, componentName);
108
- if (componentName === 'Form') this.transformLookupTypes(componentFile);
108
+
109
+ if (componentName === 'Form') {
110
+ this.transformInterfaceTypes(componentFile);
111
+ }
112
+
109
113
  assignments = this.getPropertyInitializations(propertyLines, componentName);
110
- this.addLookupImportIfRequired(componentFile);
114
+ this.addImportsDynamically(componentFile);
111
115
  }
112
116
  }
113
117
 
@@ -125,7 +129,7 @@ export class SubjectComponentGenerator {
125
129
  const subjectName: string | null = subjectMatch ? subjectMatch[1] : null;
126
130
 
127
131
  if (subjectName) {
128
- const formattedSubjectName = this._namingHandler.formatSubjectName(subjectName);
132
+ const formattedSubjectName = this._namingHandler.toPascalCase(subjectName);
129
133
  const subjectFolder = this.getSubjectFolderPath(formattedSubjectName);
130
134
  const fileName = path.basename(componentDirPath);
131
135
 
@@ -176,8 +180,17 @@ export class SubjectComponentGenerator {
176
180
  const property = line.split(':')[0].replace(/\?$/, '').trim();
177
181
  const pageLocatorString = this._selectorBuilder.getPageLocatorString(property, componentName);
178
182
 
179
- if (componentName === 'Form' && property.endsWith('Lookup')) {
180
- return ` this.${property} = createLookupDropdown(${pageLocatorString});`;
183
+ if (componentName === 'Form') {
184
+ if (property.endsWith('Lookup')) return ` this.${property} = createLookupDropdown(${pageLocatorString});`;
185
+
186
+ if (property.endsWith('DatePicker'))
187
+ return ` this.${property} = createDateControl(page, '${this._selectorBuilder.getElementId(property, componentName)}');`;
188
+
189
+ if (property.endsWith('ComboBox'))
190
+ return ` this.${property} = createFormComboBox(page, '${this._selectorBuilder.getElementId(property, componentName)}');`;
191
+
192
+ if (property.endsWith('Field'))
193
+ return ` this.${property} = createInputFieldControl(page, '${this._selectorBuilder.getElementId(property, componentName)}');`;
181
194
  }
182
195
 
183
196
  return ` this.${property} = ${pageLocatorString};`;
@@ -185,14 +198,39 @@ export class SubjectComponentGenerator {
185
198
  .join('\n');
186
199
  }
187
200
 
188
- private addLookupImportIfRequired(componentFile: { content: string }): void {
201
+ private addImportsDynamically(componentFile: { content: string }): void {
189
202
  const lookupType = 'ILookupDropdown';
203
+ const comboBoxType = 'IFormComboBox';
204
+ const inputFieldType = 'IInputFieldControl';
205
+ const datePickerType = 'IDateControl';
206
+
207
+ const inputFieldImport =
208
+ "import { createInputFieldControl, type IInputFieldControl } from '../../../../controls/InputFieldControl.js';";
209
+
190
210
  const lookupImport =
191
211
  "import { createLookupDropdown, type ILookupDropdown } from '../../../../controls/LookupDropdown.js';";
192
212
 
213
+ const comboBoxImport =
214
+ "import { createFormComboBox, type IFormComboBox } from '../../../../controls/FormComboBox.js';";
215
+
216
+ const datePickerImport =
217
+ "import { createDateControl, type IDateControl } from '../../../../controls/DateControl.js';";
218
+
193
219
  if (componentFile.content.includes(lookupType) && !componentFile.content.includes(lookupImport)) {
194
220
  componentFile.content = `${lookupImport}\n${componentFile.content}`;
195
221
  }
222
+
223
+ if (componentFile.content.includes(comboBoxType) && !componentFile.content.includes(comboBoxImport)) {
224
+ componentFile.content = `${comboBoxImport}\n${componentFile.content}`;
225
+ }
226
+
227
+ if (componentFile.content.includes(inputFieldType) && !componentFile.content.includes(inputFieldImport)) {
228
+ componentFile.content = `${inputFieldImport}\n${componentFile.content}`;
229
+ }
230
+
231
+ if (componentFile.content.includes(datePickerType) && !componentFile.content.includes(datePickerImport)) {
232
+ componentFile.content = `${datePickerImport}\n${componentFile.content}`;
233
+ }
196
234
  }
197
235
 
198
236
  private generatePropertyDeclarations(propertiesToConvert: string[], componentName: SubjectComponents): string {
@@ -204,19 +242,39 @@ export class SubjectComponentGenerator {
204
242
  return ` ${property}: ILookupDropdown;`;
205
243
  }
206
244
 
245
+ if (componentName === 'Form' && property.endsWith('ComboBox')) {
246
+ return ` ${property}: IFormComboBox;`;
247
+ }
248
+
249
+ if (componentName === 'Form' && property.endsWith('Field')) {
250
+ return ` ${property}: IInputFieldControl;`;
251
+ }
252
+
253
+ if (componentName === 'Form' && property.endsWith('DatePicker')) {
254
+ return ` ${property}: IDateControl;`;
255
+ }
256
+
207
257
  return ` ${property}: Locator;`;
208
258
  })
209
259
  .join('\n');
210
260
  }
211
261
 
212
- private transformLookupTypes(componentFile: { content: string }): void {
262
+ private transformInterfaceTypes(componentFile: { content: string }): void {
213
263
  const interfaceRegex = /export interface \w+ extends Form \{([\s\S]*?)\}/g;
214
264
 
265
+ const typeMapping: Record<string, string> = {
266
+ Lookup: 'ILookupDropdown',
267
+ ComboBox: 'IFormComboBox',
268
+ Field: 'IInputFieldControl',
269
+ DatePicker: 'IDateControl'
270
+ };
271
+
215
272
  componentFile.content = componentFile.content.replace(interfaceRegex, (match, interfaceBody) => {
216
- const propertyRegex = /(\w+Lookup)\s*:\s*Locator;/g;
273
+ const propertyRegex = /(\w+(Lookup|ComboBox|Field|DatePicker))\s*:\s*Locator;/g;
217
274
 
218
- const updatedBody = interfaceBody.replace(propertyRegex, (_propMatch: string, propName: string) => {
219
- return `${propName}: ILookupDropdown;`;
275
+ const updatedBody = interfaceBody.replace(propertyRegex, (_match: string, propName: string, suffix: string) => {
276
+ const newType = typeMapping[suffix];
277
+ return `${propName}: ${newType};`;
220
278
  });
221
279
 
222
280
  return match.replace(interfaceBody, updatedBody);
@@ -78,7 +78,7 @@ export class SubjectGenerator {
78
78
  const screenInterfaceName = this._namingHandler.generateScreenInterfaceName(subject.screentype_id);
79
79
  const screenInterfacePath = this.getScreenInterfacePath(screenInterfaceName);
80
80
  const screenFileContent = fs.readFileSync(screenInterfacePath, 'utf-8');
81
- const subjectFolderName = this._namingHandler.formatPascalCaseName(subject.subject);
81
+ const subjectFolderName = this._namingHandler.toPascalCase(subject.subject);
82
82
  const subjectFolderPath = this.getSubjectFolderRelativePath(subjectFolderName);
83
83
 
84
84
  const subjectClass = {
@@ -294,7 +294,7 @@ export class SubjectGenerator {
294
294
  }
295
295
 
296
296
  private getComponentInitializer(component: { type: string; context: string }): string {
297
- const componentNamePascal = this._namingHandler.formatPascalCaseName(component.type);
297
+ const componentNamePascal = this._namingHandler.toPascalCase(component.type);
298
298
 
299
299
  switch (component.type) {
300
300
  case 'Cardlist':
@@ -23,6 +23,20 @@ export class SubjectRegistration {
23
23
  await this.registerGeneratedSubjectsAsSubjectTypes(subjectDetails);
24
24
  }
25
25
 
26
+ public resetSubjectRegistry(): void {
27
+ const root = path.resolve(this._currentDirname, '../../');
28
+ const registryPath = path.join(root, 'page-extensions', 'SubjectRegistry.ts');
29
+ const templatePath = path.join(root, 'templates', 'SubjectRegistry.template.ts');
30
+
31
+ if (fs.existsSync(registryPath)) {
32
+ fs.rmSync(registryPath, { force: true });
33
+ }
34
+
35
+ if (fs.existsSync(templatePath)) {
36
+ fs.copyFileSync(templatePath, registryPath);
37
+ }
38
+ }
39
+
26
40
  private async registerGeneratedSubjectsAsSubjectTypes(subjectDetails: SubjectDetail[]): Promise<void> {
27
41
  const subjectTypeImportsAndExports = this.buildSubjectTypeExport(subjectDetails);
28
42
  const subject = { content: this.setSubjectTypeContent() };
@@ -1,11 +1,12 @@
1
1
  import * as path from 'node:path';
2
+ import { pascalCase, snakeCase } from 'change-case';
2
3
  import { KnownElementTypes } from '../../enums/ElementTypes.js';
3
4
  import type { ScreenComponents, SubjectComponents } from '../../types/Components.js';
4
5
 
5
6
  export class NamingHandler {
6
7
  public getDefaultElementSuffixFromComponentType(componentType: SubjectComponents): KnownElementTypes {
7
8
  const controlTypeMapping: Record<SubjectComponents, KnownElementTypes> = {
8
- Form: KnownElementTypes.Input,
9
+ Form: KnownElementTypes.Field,
9
10
  Grid: KnownElementTypes.Column
10
11
  };
11
12
 
@@ -19,7 +20,6 @@ export class NamingHandler {
19
20
  controlType = controlType.toLowerCase();
20
21
 
21
22
  const formDefaultSuffix: KnownElementTypes = this.getDefaultElementSuffixFromComponentType('Form');
22
- const gridDefaultSuffix: KnownElementTypes = this.getDefaultElementSuffixFromComponentType('Grid');
23
23
 
24
24
  const controlTypeMappingMatrix: Record<SubjectComponents, Record<string, KnownElementTypes>> = {
25
25
  Form: {
@@ -27,37 +27,41 @@ export class NamingHandler {
27
27
  phone_number: formDefaultSuffix,
28
28
  suggestion_starts_with: formDefaultSuffix,
29
29
  lookup_dropdown: KnownElementTypes.Lookup,
30
+ dropdown: KnownElementTypes.Dropdown,
31
+ lookup: KnownElementTypes.Lookup,
30
32
  checkbox: KnownElementTypes.Checkbox,
31
33
  multiline: KnownElementTypes.Textarea,
32
- combo: KnownElementTypes.Dropdown
34
+ combo: KnownElementTypes.ComboBox,
35
+ combo_box: KnownElementTypes.ComboBox,
36
+ date: KnownElementTypes.DatePicker,
37
+ date_picker: KnownElementTypes.DatePicker
33
38
  },
34
39
  Grid: {
35
- email: gridDefaultSuffix,
36
- phone_number: gridDefaultSuffix,
37
- suggestion_starts_with: gridDefaultSuffix,
38
- lookup_dropdown: gridDefaultSuffix,
39
- checkbox: gridDefaultSuffix,
40
- multiline: gridDefaultSuffix,
41
- combo: gridDefaultSuffix
40
+ // Insert Grid-specific control type mappings here
42
41
  }
43
42
  };
44
43
 
45
44
  const componentMapping = controlTypeMappingMatrix[componentType];
46
45
  return (
47
- componentMapping[controlType] || this.getDefaultElementSuffixFromComponentType(componentType)
46
+ componentMapping[controlType.toLowerCase()] || this.getDefaultElementSuffixFromComponentType(componentType)
48
47
  ).toLowerCase();
49
48
  }
50
49
 
51
50
  public getIdFromElementName(elementName: string, componentType: SubjectComponents, controlType: string): string {
52
51
  const elementSuffixToRemove = this.getElementSuffixFromControlTypeAndComponentType(controlType, componentType);
53
- const snakeCaseElementName = this.pascalToSnakeCase(elementName);
52
+ const elementSuffixToRemoveFormatted = elementSuffixToRemove.replace(/_/g, '-').toLowerCase();
54
53
 
55
- return snakeCaseElementName
56
- .replace(new RegExp(`${elementSuffixToRemove}$`, 'i'), '')
54
+ let id = this.toSnakeCase(elementName)
57
55
  .replace(/([a-z])([A-Z])/g, '$1-$2')
58
56
  .toLowerCase()
59
- .replace(/_/g, '-')
60
- .replace(/-+$/g, '');
57
+ .replace(/_/g, '-');
58
+
59
+ if (elementSuffixToRemove) {
60
+ const suffixRegex = new RegExp(`-?${elementSuffixToRemoveFormatted}$`, 'i');
61
+ id = id.replace(suffixRegex, '');
62
+ }
63
+
64
+ return id.replace(/-+/g, '-').replace(/-+$/g, '');
61
65
  }
62
66
 
63
67
  public getComponentTypeFromFileName(fileName: string): SubjectComponents {
@@ -72,7 +76,7 @@ export class NamingHandler {
72
76
  }
73
77
 
74
78
  public getElementTypeFromElementName(elementName: string): string {
75
- const snakeCaseElementName = this.pascalToSnakeCase(elementName);
79
+ const snakeCaseElementName = this.toSnakeCase(elementName);
76
80
  const parts = snakeCaseElementName.split('_');
77
81
 
78
82
  for (let i = parts.length - 1; i > 0; i--) {
@@ -91,24 +95,11 @@ export class NamingHandler {
91
95
  }
92
96
 
93
97
  public getPascalSubjectComponentName(file: string): string {
94
- return this.formatPascalCaseName(path.basename(file, '.json'));
95
- }
96
-
97
- public formatPascalCaseName(name: string): string {
98
- if (!name.includes('_')) {
99
- return name.charAt(0).toUpperCase() + name.slice(1);
100
- }
101
-
102
- return name
103
- .toLowerCase()
104
- .split('_')
105
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
106
- .join('');
98
+ return this.toPascalCase(path.basename(file, '.json'));
107
99
  }
108
100
 
109
- public formatSubjectName(subjectName: string): string {
110
- subjectName.replace(/_([a-z])/g, (g: string) => g[1].toUpperCase());
111
- return subjectName.charAt(0).toUpperCase() + subjectName.slice(1);
101
+ public toPascalCase(name: string): string {
102
+ return pascalCase(name);
112
103
  }
113
104
 
114
105
  public formatPropertyName(name: string): string {
@@ -116,30 +107,11 @@ export class NamingHandler {
116
107
  }
117
108
 
118
109
  public generateScreenInterfaceName(screentypeId: string): string {
119
- return `I${screentypeId
120
- .toLowerCase()
121
- .split('_')
122
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
123
- .join('')}`;
124
- }
125
-
126
- public snakeToPascalCase(value: string): string {
127
- if (!value.includes('_')) {
128
- return value.charAt(0).toUpperCase() + value.slice(1);
129
- }
130
-
131
- return value
132
- .toLowerCase()
133
- .split('_')
134
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
135
- .join('');
110
+ return `I${this.toPascalCase(screentypeId)}`;
136
111
  }
137
112
 
138
- public pascalToSnakeCase(value: string): string {
139
- return value
140
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
141
- .replace(/([a-z\d])([A-Z])/g, '$1_$2')
142
- .toLowerCase();
113
+ public toSnakeCase(value: string): string {
114
+ return snakeCase(value);
143
115
  }
144
116
 
145
117
  public getComponentIdFromName(componentName: string, componentType: ScreenComponents): string {
@@ -181,6 +153,6 @@ export class NamingHandler {
181
153
  public generateSubjectClassName(subject: { subject: string; screentype_context: string; variant: string }): string {
182
154
  const uniqueSubjectIdentifier = subject.variant !== '' ? `${subject.subject}_${subject.variant}` : subject.subject;
183
155
  const className = `${uniqueSubjectIdentifier}_${subject.screentype_context}`;
184
- return this.formatPascalCaseName(className);
156
+ return this.toPascalCase(className);
185
157
  }
186
158
  }
@@ -26,6 +26,7 @@ export class ExportComponent {
26
26
  public async selectColumns(columns: string[]): Promise<void> {
27
27
  for (const column of columns) {
28
28
  const checkbox = this._objects.selectColumnsCheckbox(column);
29
+ await this._page.waitForTimeout(100);
29
30
  await checkbox.check();
30
31
  }
31
32
  }
@@ -12,7 +12,7 @@ export class BaseTabObjects extends BaseComponentObjects {
12
12
  public tabList = (): Locator => this.context.getByRole('tablist');
13
13
 
14
14
  public tabByTestId = (name: string): Locator =>
15
- this.tabList().locator(`[role="tab"][data-testid^="tabstrip__tab__${name.toLowerCase()}"]`);
15
+ this.tabList().locator(`[role="tab"][data-testid^="tabstrip__tab__${name.toLowerCase()}__"]`);
16
16
 
17
17
  // use role for generic index-based selection
18
18
  public tabByIndex = (index: number): Locator => this.tabList().getByRole('tab').nth(index);
@@ -0,0 +1,67 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /* biome-ignore-all lint/suspicious/noExplicitAny: reason42 */
3
+ import type { Locator, Page } from '@playwright/test';
4
+
5
+ export type IDateControl = Locator & IDateControlCore;
6
+
7
+ export type IDateControlCore = {
8
+ fill(value: string): Promise<void>;
9
+ clear(): Promise<void>;
10
+ inputValue(): Promise<string>;
11
+ inputElement(): Locator;
12
+ };
13
+
14
+ export class DateControl implements IDateControlCore {
15
+ private _dateFieldLocator: Locator;
16
+ private _inputLocator: Locator;
17
+
18
+ constructor(page: Page, id: string) {
19
+ this._dateFieldLocator = page.getByTestId(`form-field__${id}`);
20
+ this._inputLocator = this._dateFieldLocator.locator('input');
21
+ }
22
+
23
+ /**
24
+ * Enter a date value in any common format (e.g., "MM/DD/YYYY", "YYYY-MM-DD", "DD-MM-YYYY").
25
+ * The method will extract the numbers and input them sequentially, allowing the date picker to format it correctly.
26
+ * For example, entering "01012025" will be interpreted as "01/01/2025".
27
+ * This approach provides flexibility in how dates can be entered while ensuring compatibility with various date picker implementations.
28
+ * @param value
29
+ */
30
+ public async fill(value: string): Promise<void> {
31
+ const numbersOnlyValue = value.replace(/\D/g, '');
32
+
33
+ await this._inputLocator.focus();
34
+ await this._inputLocator.pressSequentially(numbersOnlyValue);
35
+ }
36
+
37
+ public async clear(): Promise<void> {
38
+ await this._inputLocator.fill('');
39
+ }
40
+
41
+ public async inputValue(): Promise<string> {
42
+ return await this._inputLocator.inputValue();
43
+ }
44
+
45
+ public inputElement(): Locator {
46
+ return this._inputLocator;
47
+ }
48
+ }
49
+
50
+ export function createDateControl(page: Page, id: string): IDateControl {
51
+ const inputFieldControl = new DateControl(page, id);
52
+ const locator = page.getByTestId(`form-field__${id}`);
53
+
54
+ return new Proxy(inputFieldControl, {
55
+ get(target, prop, receiver) {
56
+ if (prop in target) {
57
+ return Reflect.get(target, prop, receiver);
58
+ }
59
+ const value = (locator as any)[prop];
60
+
61
+ if (typeof value === 'function') {
62
+ return value.bind(locator);
63
+ }
64
+ return value;
65
+ }
66
+ }) as unknown as IDateControl;
67
+ }
@@ -0,0 +1,74 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /* biome-ignore-all lint/suspicious/noExplicitAny: Reason42 */
3
+ import type { Locator, Page } from '@playwright/test';
4
+
5
+ export type IFormComboBox = Locator & IFormComboBoxCore;
6
+
7
+ export type IFormComboBoxCore = {
8
+ lookupSelect(optionToSelect: string): Promise<void>;
9
+ selectOption(optionText: string): Promise<void>;
10
+ getValidationMessage(): Promise<string>;
11
+ clearByClick(): Promise<void>;
12
+ };
13
+
14
+ class FormComboBox implements IFormComboBoxCore {
15
+ private _formFieldLocator: Locator;
16
+ private _autoCompleteMenu: Locator;
17
+ private _id: string;
18
+
19
+ constructor(page: Page, id: string) {
20
+ this._id = id;
21
+ this._formFieldLocator = page.getByTestId(`form-field__${id}`);
22
+ this._autoCompleteMenu = page.getByTestId(`form-field__${id}__select__options-list`);
23
+ }
24
+
25
+ async selectOption(optionText: string): Promise<void> {
26
+ await this._formFieldLocator.locator('input').click();
27
+
28
+ await this._autoCompleteMenu.waitFor({ state: 'visible' });
29
+
30
+ const optionTextLowercase = optionText.toLowerCase();
31
+
32
+ await this._autoCompleteMenu.getByTestId(`form-field__${this._id}__select__${optionTextLowercase}__text`).click();
33
+ }
34
+
35
+ async getValidationMessage(): Promise<string> {
36
+ await this._formFieldLocator.locator('.TooltipContainer p').waitFor({ state: 'visible' });
37
+ return await this._formFieldLocator.locator('.TooltipContainer p').innerText();
38
+ }
39
+ async clearByClick(): Promise<void> {
40
+ await this._formFieldLocator.locator('Button[aria-label="Clear"]').click();
41
+ }
42
+
43
+ async lookupSelect(optionToSelect: string): Promise<void> {
44
+ await this._formFieldLocator.locator('input').fill(optionToSelect);
45
+
46
+ await this._autoCompleteMenu.waitFor({ state: 'visible' });
47
+
48
+ const optionToSelectLowercase = optionToSelect.toLowerCase();
49
+
50
+ await this._autoCompleteMenu
51
+ .getByTestId(`form-field__${this._id}__select__${optionToSelectLowercase}__text`)
52
+ .click();
53
+ }
54
+ }
55
+
56
+ export function createFormComboBox(page: Page, id: string): IFormComboBox {
57
+ const comboBox = new FormComboBox(page, id);
58
+ const locator = page.getByTestId(`form-field__${id}`);
59
+
60
+ return new Proxy(comboBox, {
61
+ get(target, prop, receiver) {
62
+ if (prop in target) {
63
+ return Reflect.get(target, prop, receiver);
64
+ }
65
+
66
+ const value = (locator as any)[prop];
67
+
68
+ if (typeof value === 'function') {
69
+ return value.bind(locator);
70
+ }
71
+ return value;
72
+ }
73
+ }) as unknown as IFormComboBox;
74
+ }
@@ -0,0 +1,79 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /** biome-ignore-all lint/suspicious/noExplicitAny: reason42 */
3
+ import type { Locator, Page } from '@playwright/test';
4
+
5
+ export type IInputFieldControl = Locator & ICoreInputFieldControl;
6
+
7
+ export type ICoreInputFieldControl = {
8
+ getValidationMessage(): Promise<string>;
9
+ getLabelText(): Promise<string>;
10
+ fill(value: string): Promise<void>;
11
+ click(): Promise<void>;
12
+ clear(): Promise<void>;
13
+ inputValue(): Promise<string>;
14
+ inputElement(): Locator;
15
+ };
16
+
17
+ export class InputFieldControl implements ICoreInputFieldControl {
18
+ private _formFieldLocator: Locator;
19
+ private _inputLocator: Locator;
20
+ private _id: string;
21
+
22
+ constructor(page: Page, id: string) {
23
+ this._id = id;
24
+ this._formFieldLocator = page.getByTestId(`form-field__${id}`);
25
+ this._inputLocator = this._formFieldLocator.locator('input');
26
+ }
27
+
28
+ public async getValidationMessage(): Promise<string> {
29
+ await this._formFieldLocator.locator('.TooltipContainer p').waitFor({ state: 'visible' });
30
+ return await this._formFieldLocator.locator('.TooltipContainer p').innerText();
31
+ }
32
+
33
+ public async fill(value: string): Promise<void> {
34
+ await this._inputLocator.fill(value);
35
+ }
36
+
37
+ public async click(): Promise<void> {
38
+ await this._inputLocator.click();
39
+ }
40
+
41
+ public async clear(): Promise<void> {
42
+ await this._inputLocator.fill('');
43
+ }
44
+
45
+ public async inputValue(): Promise<string> {
46
+ return await this._inputLocator.inputValue();
47
+ }
48
+
49
+ public inputElement(): Locator {
50
+ return this._inputLocator;
51
+ }
52
+
53
+ public async getLabelText(): Promise<string> {
54
+ let returnText = (await this._formFieldLocator.getByTestId(`form-field__${this._id}__label`).textContent()) || '';
55
+
56
+ returnText = returnText.replace(/\s+/g, ' ').trim();
57
+
58
+ return returnText;
59
+ }
60
+ }
61
+
62
+ export function createInputFieldControl(page: Page, id: string): IInputFieldControl {
63
+ const inputFieldControl = new InputFieldControl(page, id);
64
+ const locator = page.getByTestId(`form-field__${id}`);
65
+
66
+ return new Proxy(inputFieldControl, {
67
+ get(target, prop, receiver) {
68
+ if (prop in target) {
69
+ return Reflect.get(target, prop, receiver);
70
+ }
71
+ const value = (locator as any)[prop];
72
+
73
+ if (typeof value === 'function') {
74
+ return value.bind(locator);
75
+ }
76
+ return value;
77
+ }
78
+ }) as unknown as IInputFieldControl;
79
+ }
@@ -3,12 +3,14 @@
3
3
  import type { Locator } from '@playwright/test';
4
4
  import { escapeRegex } from '../helpers/RegexHelper.js';
5
5
 
6
- export type ILookupDropdown = Locator & {
6
+ export type ILookupDropdown = Locator & ILookupDropdownCore;
7
+
8
+ export type ILookupDropdownCore = {
7
9
  lookupSelect(optionToSelect: string): Promise<void>;
8
10
  selectOption(optionText: string): Promise<void>;
9
11
  };
10
12
 
11
- class LookupDropdown {
13
+ class LookupDropdown implements ILookupDropdownCore {
12
14
  private _locator: Locator;
13
15
 
14
16
  constructor(locator: Locator) {
package/controls/index.ts CHANGED
@@ -1 +1,4 @@
1
+ export * from './DateControl.js';
2
+ export * from './FormComboBox.js';
3
+ export * from './InputFieldControl.js';
1
4
  export * from './LookupDropdown.js';