@thinkwise/testwise 0.2.0-beta.3 → 0.2.5

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 (136) hide show
  1. package/Testwise.ts +8 -7
  2. package/artifact-builder/ArtifactManager.ts +0 -1
  3. package/artifact-builder/InterfaceGenerator.ts +73 -83
  4. package/artifact-builder/ModelDataBuilder.ts +74 -86
  5. package/artifact-builder/ModelDataRefiner.ts +18 -13
  6. package/artifact-builder/SchemaGenerator.ts +1 -4
  7. package/artifact-builder/ScreenInterfaceRefiner.ts +0 -5
  8. package/artifact-builder/SelectorBuilder.ts +8 -1
  9. package/artifact-builder/SubjectComponentGenerator.ts +77 -7
  10. package/artifact-builder/SubjectGenerator.ts +3 -3
  11. package/artifact-builder/SubjectRegistration.ts +69 -61
  12. package/artifact-builder/helpers/DataRetriever.ts +6 -3
  13. package/artifact-builder/helpers/NamingHandler.ts +28 -20
  14. package/artifact-builder/helpers/Stopwatch.ts +13 -0
  15. package/artifact-builder/helpers/index.ts +1 -0
  16. package/components/BaseComponentObjects.ts +4 -1
  17. package/components/action-bar/ActionBar.ts +3 -3
  18. package/components/grid/Grid.ts +18 -21
  19. package/components/tab/BaseTab.ts +2 -2
  20. package/components/tab/BaseTabObjects.ts +1 -1
  21. package/components/tab/DetailTabPage.ts +2 -2
  22. package/components/tab/Tab.ts +4 -4
  23. package/controls/LookupDropdown.ts +7 -2
  24. package/dist/Testwise.d.ts +1 -0
  25. package/dist/Testwise.js +15 -6
  26. package/dist/Testwise.js.map +1 -1
  27. package/dist/artifact-builder/ArtifactManager.js.map +1 -1
  28. package/dist/artifact-builder/InterfaceGenerator.d.ts +1 -1
  29. package/dist/artifact-builder/InterfaceGenerator.js +60 -67
  30. package/dist/artifact-builder/InterfaceGenerator.js.map +1 -1
  31. package/dist/artifact-builder/ModelDataBuilder.js +49 -60
  32. package/dist/artifact-builder/ModelDataBuilder.js.map +1 -1
  33. package/dist/artifact-builder/ModelDataRefiner.js +11 -7
  34. package/dist/artifact-builder/ModelDataRefiner.js.map +1 -1
  35. package/dist/artifact-builder/SchemaGenerator.js +0 -2
  36. package/dist/artifact-builder/SchemaGenerator.js.map +1 -1
  37. package/dist/artifact-builder/ScreenInterfaceRefiner.js +0 -5
  38. package/dist/artifact-builder/ScreenInterfaceRefiner.js.map +1 -1
  39. package/dist/artifact-builder/SelectorBuilder.js +3 -1
  40. package/dist/artifact-builder/SelectorBuilder.js.map +1 -1
  41. package/dist/artifact-builder/SubjectComponentGenerator.d.ts +4 -0
  42. package/dist/artifact-builder/SubjectComponentGenerator.js +61 -5
  43. package/dist/artifact-builder/SubjectComponentGenerator.js.map +1 -1
  44. package/dist/artifact-builder/SubjectGenerator.js +1 -1
  45. package/dist/artifact-builder/SubjectGenerator.js.map +1 -1
  46. package/dist/artifact-builder/SubjectRegistration.d.ts +10 -11
  47. package/dist/artifact-builder/SubjectRegistration.js +52 -44
  48. package/dist/artifact-builder/SubjectRegistration.js.map +1 -1
  49. package/dist/artifact-builder/helpers/DataRetriever.js +4 -3
  50. package/dist/artifact-builder/helpers/DataRetriever.js.map +1 -1
  51. package/dist/artifact-builder/helpers/NamingHandler.d.ts +2 -1
  52. package/dist/artifact-builder/helpers/NamingHandler.js +23 -16
  53. package/dist/artifact-builder/helpers/NamingHandler.js.map +1 -1
  54. package/dist/artifact-builder/helpers/Stopwatch.d.ts +5 -0
  55. package/dist/artifact-builder/helpers/Stopwatch.js +13 -0
  56. package/dist/artifact-builder/helpers/Stopwatch.js.map +1 -0
  57. package/dist/artifact-builder/helpers/index.d.ts +1 -0
  58. package/dist/artifact-builder/helpers/index.js +1 -0
  59. package/dist/artifact-builder/helpers/index.js.map +1 -1
  60. package/dist/components/BaseComponentObjects.d.ts +1 -0
  61. package/dist/components/BaseComponentObjects.js +3 -1
  62. package/dist/components/BaseComponentObjects.js.map +1 -1
  63. package/dist/components/action-bar/ActionBar.d.ts +1 -1
  64. package/dist/components/action-bar/ActionBar.js +3 -3
  65. package/dist/components/grid/Grid.d.ts +1 -0
  66. package/dist/components/grid/Grid.js +4 -1
  67. package/dist/components/grid/Grid.js.map +1 -1
  68. package/dist/components/tab/BaseTab.d.ts +2 -2
  69. package/dist/components/tab/BaseTab.js +2 -2
  70. package/dist/components/tab/BaseTab.js.map +1 -1
  71. package/dist/components/tab/BaseTabObjects.js +1 -1
  72. package/dist/components/tab/BaseTabObjects.js.map +1 -1
  73. package/dist/components/tab/DetailTabPage.d.ts +2 -2
  74. package/dist/components/tab/DetailTabPage.js +2 -2
  75. package/dist/components/tab/DetailTabPage.js.map +1 -1
  76. package/dist/components/tab/Tab.d.ts +3 -3
  77. package/dist/components/tab/Tab.js +4 -4
  78. package/dist/components/tab/Tab.js.map +1 -1
  79. package/dist/controls/LookupDropdown.d.ts +3 -7
  80. package/dist/controls/LookupDropdown.js.map +1 -1
  81. package/dist/enums/ElementTypes.d.ts +1 -1
  82. package/dist/enums/ElementTypes.js +1 -1
  83. package/dist/enums/ElementTypes.js.map +1 -1
  84. package/dist/helpers/ConfigChecker.d.ts +3 -0
  85. package/dist/helpers/ConfigChecker.js +7 -0
  86. package/dist/helpers/ConfigChecker.js.map +1 -0
  87. package/dist/helpers/LoginHelper.js +1 -1
  88. package/dist/helpers/LoginHelper.js.map +1 -1
  89. package/dist/interfaces/IComponentObjects.d.ts +1 -0
  90. package/dist/page-extensions/SubjectRegistry.d.ts +0 -8
  91. package/dist/page-extensions/SubjectRegistry.js +2 -6
  92. package/dist/page-extensions/SubjectRegistry.js.map +1 -1
  93. package/dist/page-extensions/index.d.ts +0 -1
  94. package/dist/page-extensions/index.js +0 -1
  95. package/dist/page-extensions/index.js.map +1 -1
  96. package/dist/services/IndiciumApi.service.d.ts +27 -0
  97. package/dist/services/IndiciumApi.service.js +135 -0
  98. package/dist/services/IndiciumApi.service.js.map +1 -0
  99. package/dist/templates/test-artifacts/SubjectPageBase.d.ts +5 -0
  100. package/dist/templates/test-artifacts/SubjectPageBase.js +6 -0
  101. package/dist/templates/test-artifacts/SubjectPageBase.js.map +1 -0
  102. package/dist/templates/test-artifacts/screens/index.d.ts +1 -0
  103. package/dist/templates/test-artifacts/screens/index.js +2 -0
  104. package/dist/templates/test-artifacts/screens/index.js.map +1 -0
  105. package/dist/templates/test-artifacts/subjects/index.d.ts +1 -0
  106. package/dist/templates/test-artifacts/subjects/index.js +2 -0
  107. package/dist/templates/test-artifacts/subjects/index.js.map +1 -0
  108. package/enums/ElementTypes.ts +2 -2
  109. package/helpers/ConfigChecker.ts +7 -0
  110. package/helpers/LoginHelper.ts +1 -1
  111. package/interfaces/IComponentObjects.ts +1 -0
  112. package/interfaces/IRegisteredSubjects.ts +1 -1
  113. package/package.json +5 -3
  114. package/page-extensions/SubjectRegistry.ts +2 -19
  115. package/page-extensions/index.ts +0 -1
  116. package/scripts/main.js +63 -82
  117. package/scripts/postinstall.js +40 -42
  118. package/scripts/setup.js +37 -37
  119. package/scripts/sync.js +726 -39
  120. package/services/ConfigBuilder.ts +1 -1
  121. package/services/IndiciumApi.service.ts +159 -0
  122. package/templates/SubjectRegistry.template.ts +73 -0
  123. package/templates/test-artifacts/SubjectPageBase.ts +9 -0
  124. package/templates/test-artifacts/screens/index.ts +0 -0
  125. package/templates/test-artifacts/subjects/index.ts +0 -0
  126. package/tsconfig.json +2 -3
  127. package/types/Components.ts +1 -1
  128. package/dist/config.json +0 -10
  129. package/dist/page-extensions/SubjectProvider.d.ts +0 -11
  130. package/dist/page-extensions/SubjectProvider.js +0 -24
  131. package/dist/page-extensions/SubjectProvider.js.map +0 -1
  132. package/dist/test-artifacts/index.d.ts +0 -3
  133. package/dist/test-artifacts/index.js +0 -4
  134. package/dist/test-artifacts/index.js.map +0 -1
  135. package/page-extensions/SubjectProvider.ts +0 -41
  136. package/test-artifacts/index.ts +0 -3
@@ -85,6 +85,10 @@ export class SubjectComponentGenerator {
85
85
  this.addComponentImports(componentFile, componentName);
86
86
  this.setSubjectExtendsComponent(componentFile, componentName);
87
87
 
88
+ if (componentName === 'Grid') {
89
+ this.transformGridInterface(componentFile);
90
+ }
91
+
88
92
  const interfaceMatches: RegExpMatchArray | null = componentFile.content.match(/export interface (\w+) extends/);
89
93
 
90
94
  if (interfaceMatches) {
@@ -92,16 +96,29 @@ export class SubjectComponentGenerator {
92
96
  const matchingProperties = this.getPropertyNamesFromInterface(componentFile, mainInterface);
93
97
  let propertyDeclarations = '';
94
98
  let assignments = '';
99
+ let gridGetters = '';
95
100
 
96
101
  if (matchingProperties?.[1]) {
97
102
  const propertyLines: string[] = this.convertStringsToArray(matchingProperties[1]);
98
103
 
99
- propertyDeclarations = this.generatePropertyDeclarations(propertyLines);
100
-
101
- assignments = this.getPropertyInitializations(propertyLines, componentName);
104
+ if (componentName === 'Grid') {
105
+ gridGetters = this.generateGridGetters(propertyLines, componentName);
106
+ } else {
107
+ propertyDeclarations = this.generatePropertyDeclarations(propertyLines, componentName);
108
+ if (componentName === 'Form') this.transformLookupTypes(componentFile);
109
+ assignments = this.getPropertyInitializations(propertyLines, componentName);
110
+ this.addLookupImportIfRequired(componentFile);
111
+ }
102
112
  }
103
113
 
104
- componentFile.content += `\nexport class ${mainInterface}Component extends ${componentName} implements ${mainInterface} {\n${propertyDeclarations}\n\n constructor(page: Page, context: Locator | null = null) {\n super(page, context);\n${assignments}\n }\n}\n`;
114
+ const classBody =
115
+ componentName === 'Grid'
116
+ ? `\nexport class ${mainInterface}Component extends ${componentName} implements ${mainInterface} {\n\n` +
117
+ ` constructor(page: Page, context: Locator | null = null) {\n super(page, context);\n }\n\n${gridGetters}}\n`
118
+ : `\nexport class ${mainInterface}Component extends ${componentName} implements ${mainInterface} {\n${propertyDeclarations}\n\n` +
119
+ ` constructor(page: Page, context: Locator | null = null) {\n super(page, context);\n${assignments}\n }\n}\n`;
120
+
121
+ componentFile.content += classBody;
105
122
  }
106
123
 
107
124
  const subjectMatch: RegExpMatchArray | null = componentDirPath.match(/subjects[/](\w+)[/]/);
@@ -113,7 +130,6 @@ export class SubjectComponentGenerator {
113
130
  const fileName = path.basename(componentDirPath);
114
131
 
115
132
  this.verifyDirectoryExists(subjectFolder);
116
-
117
133
  fs.writeFileSync(path.join(subjectFolder, fileName), componentFile.content, 'utf-8');
118
134
  } else {
119
135
  fs.writeFileSync(componentDirPath, componentFile.content, 'utf-8');
@@ -121,6 +137,26 @@ export class SubjectComponentGenerator {
121
137
  }
122
138
  }
123
139
 
140
+ private transformGridInterface(componentFile: { content: string }): void {
141
+ const interfaceRegex = /export interface \w+ extends Grid \{([\s\S]*?)\}/g;
142
+
143
+ componentFile.content = componentFile.content.replace(interfaceRegex, (match, body) => {
144
+ const transformedBody = body.replace(/(\w+)\s*:\s*Locator(?:\[\])?;/g, 'readonly $1: Promise<Locator[]>;');
145
+ return match.replace(body, transformedBody);
146
+ });
147
+ }
148
+
149
+ private generateGridGetters(propertyLines: string[], componentName: SubjectComponents): string {
150
+ return propertyLines
151
+ .map((line) => {
152
+ const property = line.replace('readonly', '').split(':')[0].trim();
153
+ const pageLocatorString = this._selectorBuilder.getPageLocatorString(property, componentName);
154
+
155
+ return ` get ${property}(): Promise<Locator[]> {\n return this.${pageLocatorString}.all();\n }\n`;
156
+ })
157
+ .join('\n');
158
+ }
159
+
124
160
  private getSubjectFolderPath(formattedSubjectName: string): string {
125
161
  return path.resolve(
126
162
  this._currentDirname,
@@ -139,20 +175,54 @@ export class SubjectComponentGenerator {
139
175
  .map((line) => {
140
176
  const property = line.split(':')[0].replace(/\?$/, '').trim();
141
177
  const pageLocatorString = this._selectorBuilder.getPageLocatorString(property, componentName);
178
+
179
+ if (componentName === 'Form' && property.endsWith('Lookup')) {
180
+ return ` this.${property} = createLookupDropdown(${pageLocatorString});`;
181
+ }
182
+
142
183
  return ` this.${property} = ${pageLocatorString};`;
143
184
  })
144
185
  .join('\n');
145
186
  }
146
187
 
147
- private generatePropertyDeclarations(propertiesToConvert: string[]): string {
188
+ private addLookupImportIfRequired(componentFile: { content: string }): void {
189
+ const lookupType = 'ILookupDropdown';
190
+ const lookupImport =
191
+ "import { createLookupDropdown, type ILookupDropdown } from '../../../../controls/LookupDropdown.js';";
192
+
193
+ if (componentFile.content.includes(lookupType) && !componentFile.content.includes(lookupImport)) {
194
+ componentFile.content = `${lookupImport}\n${componentFile.content}`;
195
+ }
196
+ }
197
+
198
+ private generatePropertyDeclarations(propertiesToConvert: string[], componentName: SubjectComponents): string {
148
199
  return propertiesToConvert
149
200
  .map((line) => {
150
201
  const property = line.replace(/^\s+/, '').split(':')[0].replace(/\?$/, '').trim();
151
- return ` ${property}!: Locator;`;
202
+
203
+ if (componentName === 'Form' && property.endsWith('Lookup')) {
204
+ return ` ${property}: ILookupDropdown;`;
205
+ }
206
+
207
+ return ` ${property}: Locator;`;
152
208
  })
153
209
  .join('\n');
154
210
  }
155
211
 
212
+ private transformLookupTypes(componentFile: { content: string }): void {
213
+ const interfaceRegex = /export interface \w+ extends Form \{([\s\S]*?)\}/g;
214
+
215
+ componentFile.content = componentFile.content.replace(interfaceRegex, (match, interfaceBody) => {
216
+ const propertyRegex = /(\w+Lookup)\s*:\s*Locator;/g;
217
+
218
+ const updatedBody = interfaceBody.replace(propertyRegex, (_propMatch: string, propName: string) => {
219
+ return `${propName}: ILookupDropdown;`;
220
+ });
221
+
222
+ return match.replace(interfaceBody, updatedBody);
223
+ });
224
+ }
225
+
156
226
  private convertStringsToArray(content: string): string[] {
157
227
  return content
158
228
  .split('\n')
@@ -57,7 +57,7 @@ export class SubjectGenerator {
57
57
  const tsFiles = fs.readdirSync(subjectFolderPath).filter((file) => file.endsWith('.ts') && file !== 'index.ts');
58
58
  const indexFile = path.join(subjectFolderPath, 'index.ts');
59
59
 
60
- if(tsFiles.length === 0) return;
60
+ if (tsFiles.length === 0) return;
61
61
 
62
62
  if (!fs.existsSync(indexFile)) {
63
63
  fs.writeFileSync(indexFile, '', 'utf-8');
@@ -65,7 +65,7 @@ export class SubjectGenerator {
65
65
 
66
66
  tsFiles.forEach((tsFile) => {
67
67
  if (tsFile === 'index.ts') return;
68
-
68
+
69
69
  const className = tsFile.replace('.ts', '');
70
70
  exportLines.push(`export { ${className} } from './${className}.js';`);
71
71
  });
@@ -82,7 +82,7 @@ export class SubjectGenerator {
82
82
  const subjectFolderPath = this.getSubjectFolderRelativePath(subjectFolderName);
83
83
 
84
84
  const subjectClass = {
85
- content: `import type { Page } from '@playwright/test';\nimport { SubjectPageBase } from '../../index.js';\n`,
85
+ content: `import type { Page } from '@playwright/test';\nimport { SubjectPageBase } from '../../SubjectPageBase.js';\n`,
86
86
  name: this._namingHandler.generateSubjectClassName(subject)
87
87
  };
88
88
  const componentDictionary: { name: string; type: SubjectAgnosticComponents; context: string }[] =
@@ -2,55 +2,45 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import * as prettier from 'prettier';
5
6
 
6
7
  export class SubjectRegistration {
7
- private static readonly SUBJECTS_RELATIVE_PATH = 'test-artifacts/subjects';
8
8
  private static readonly IMPORT_MATCH_REGEX = /import .*'\.\.\/test-artifacts\/subjects\/index\.js';\n?/g;
9
9
 
10
10
  private readonly _currentFilename: string;
11
11
  private readonly _currentDirname: string;
12
12
  private readonly _subjectTypePath: string;
13
- private readonly _subjectsIndexDirectory: string;
14
- private readonly _subjectBuilderPath: string;
15
13
 
16
14
  constructor() {
17
15
  this._currentFilename = fileURLToPath(import.meta.url);
18
16
  this._currentDirname = dirname(this._currentFilename);
19
17
  this._subjectTypePath = path.resolve(this._currentDirname, '../../page-extensions/SubjectRegistry.ts');
20
- this._subjectsIndexDirectory = path.resolve(
21
- this._currentDirname,
22
- `../../${SubjectRegistration.SUBJECTS_RELATIVE_PATH}/index.ts`
23
- );
24
- this._subjectBuilderPath = path.resolve(this._currentDirname, '../../page-extensions/SubjectProvider.ts');
25
18
  }
26
19
 
27
- public run(): void {
28
- this.registerGeneratedSubjectsAsSubjectTypes();
29
- this.registerGeneratedSubjectsUnderPageGet();
20
+ public async run(): Promise<void> {
21
+ const subjectDetails = this.getAllSubjectDetails();
22
+
23
+ await this.registerGeneratedSubjectsAsSubjectTypes(subjectDetails);
30
24
  }
31
25
 
32
- private registerGeneratedSubjectsAsSubjectTypes(): void {
33
- const subjectList = this.getAllSubjects();
34
- const subjectTypeImportsAndExports = this.getSubjectTypesImportsAndExports(subjectList);
26
+ private async registerGeneratedSubjectsAsSubjectTypes(subjectDetails: SubjectDetail[]): Promise<void> {
27
+ const subjectTypeImportsAndExports = this.buildSubjectTypeExport(subjectDetails);
35
28
  const subject = { content: this.setSubjectTypeContent() };
36
29
 
37
30
  this.removeExistingSubjectTypeDefinition(subject);
38
31
  this.removeAllImportsFromSubjectIndex(subject);
39
32
 
40
33
  subject.content = subjectTypeImportsAndExports + subject.content;
41
- fs.writeFileSync(this._subjectTypePath, subject.content, 'utf-8');
42
- }
43
-
44
- private registerGeneratedSubjectsUnderPageGet(): void {
45
- const subjectNames = this.getAllSubjects();
46
- const subjectBuilder = { content: fs.readFileSync(this._subjectBuilderPath, 'utf-8') };
34
+ subject.content = this.addPathRecordsToContent(subject.content, subjectDetails);
47
35
 
48
- this.removeSubjectBuilderImports(subjectBuilder);
49
- this.removeSubjectBuilderSubjectDefinition(subjectBuilder);
50
- this.addSubjectBuilderImports(subjectNames, subjectBuilder);
51
- this.addSubjectBuilderSubjectDefinition(subjectNames, subjectBuilder);
36
+ const formattedContent = await prettier.format(subject.content, {
37
+ parser: 'typescript',
38
+ singleQuote: true,
39
+ semi: true,
40
+ trailingComma: 'all'
41
+ });
52
42
 
53
- fs.writeFileSync(this._subjectBuilderPath, subjectBuilder.content, 'utf-8');
43
+ fs.writeFileSync(this._subjectTypePath, formattedContent, 'utf-8');
54
44
  }
55
45
 
56
46
  private removeExistingSubjectTypeDefinition(subject: { content: string }): void {
@@ -71,12 +61,15 @@ export class SubjectRegistration {
71
61
  }
72
62
  }
73
63
 
74
- private getSubjectTypesImportsAndExports(subjectList: string[]): string {
75
- const importLine = `import type { ${subjectList.join(', ')} } from '../${SubjectRegistration.SUBJECTS_RELATIVE_PATH}/index.js';`;
76
- const subjectTypeEntries = subjectList.map((name) => ` ${name}: typeof ${name};`).join('\n ');
77
- const newSubjectTypes = `${importLine}
78
-
79
- export type SubjectType = {
64
+ private buildSubjectTypeExport(subjectDetails: SubjectDetail[]): string {
65
+ const subjectTypeEntries = subjectDetails
66
+ .map(
67
+ (details) =>
68
+ ` ${details.subject}${details.variant ?? ''}${details.screen}: ` +
69
+ `Promise<import('../test-artifacts/subjects/${details.subject}/index.js').${details.subject}${details.variant ?? ''}${details.screen}>;`
70
+ )
71
+ .join('\n ');
72
+ const newSubjectTypes = `export type SubjectType = {
80
73
  ${subjectTypeEntries}
81
74
  };
82
75
  `;
@@ -84,53 +77,68 @@ export class SubjectRegistration {
84
77
  return newSubjectTypes;
85
78
  }
86
79
 
87
- private getAllSubjects(): string[] {
80
+ private addPathRecordsToContent(content: string, subjectDetails: SubjectDetail[]): string {
81
+ const pathRecordsString = this.buildPathRecordsString(subjectDetails);
82
+ const stringToMatch = "none: ''";
83
+ return content.replace(stringToMatch, `\n${pathRecordsString}\n `);
84
+ }
85
+
86
+ private buildPathRecordsString(subjectDetails: SubjectDetail[]): string {
87
+ return subjectDetails
88
+ .map(
89
+ (details) =>
90
+ ` ${details.subject}${details.variant ?? ''}${details.screen}: ` +
91
+ `'../test-artifacts/subjects/${details.subject}/index.js',`
92
+ )
93
+ .join('\n ');
94
+ }
95
+
96
+ private getAllSubjectDetails(): SubjectDetail[] {
88
97
  const subjectsDir = path.resolve(this._currentDirname, '../../test-artifacts/subjects');
89
98
 
90
99
  const subjectFolders = fs.readdirSync(subjectsDir).filter((file) => {
91
100
  return fs.statSync(path.join(subjectsDir, file)).isDirectory();
92
101
  });
93
102
 
94
- const subjectNames: string[] = [];
103
+ const subjectDetails: SubjectDetail[] = [];
104
+ const seenFullNames = new Set<string>();
95
105
 
96
106
  subjectFolders.forEach((folder) => {
97
107
  const folderPath = path.join(subjectsDir, folder);
98
- const tsFiles = fs.readdirSync(folderPath).filter((file) => file.endsWith('.ts'));
108
+ const tsFiles = fs.readdirSync(folderPath).filter((file) => file.endsWith('.ts') && file !== 'index.ts');
99
109
 
100
110
  tsFiles.forEach((tsFile) => {
101
111
  const classContent = fs.readFileSync(path.join(folderPath, tsFile), 'utf-8');
102
112
  const classMatches = classContent.matchAll(/export\s+class\s+(\w+)/g);
113
+
103
114
  for (const match of classMatches) {
104
- subjectNames.push(match[1]);
115
+ const className = match[1];
116
+ const pattern = new RegExp(`^(${folder})(.*)(Main|Popup|Zoom|Detail)$`);
117
+ const variantMatch = className.match(pattern);
118
+
119
+ if (variantMatch) {
120
+ const fullName = variantMatch[0];
121
+ if (!seenFullNames.has(fullName)) {
122
+ const variant = variantMatch[2] || '';
123
+ const screen = variantMatch[3] as SubjectDetail['screen'];
124
+
125
+ subjectDetails.push({
126
+ subject: folder,
127
+ variant: variant,
128
+ screen: screen
129
+ });
130
+ seenFullNames.add(fullName);
131
+ }
132
+ }
105
133
  }
106
134
  });
107
135
  });
108
- return subjectNames;
109
- }
110
-
111
- private addSubjectBuilderSubjectDefinition(subjectNames: string[], subjectBuilder: { content: string }): void {
112
- const pageSubjectDefinition = `page.subject = {\n${subjectNames.map((name) => ` ${name},`).join('\n')}\n};\n`;
113
-
114
- subjectBuilder.content = subjectBuilder.content.replace(
115
- /(\/\/ page\.subject definition goes here\n)/,
116
- `$1 ${pageSubjectDefinition} `
117
- );
118
- }
119
-
120
- private removeSubjectBuilderSubjectDefinition(subjectBuilder: { content: string }): void {
121
- subjectBuilder.content = subjectBuilder.content.replace(/page\.subject\s*=\s*{[\s\S]*?};\s*/m, '');
122
- }
123
-
124
- private addSubjectBuilderImports(subjectNames: string[], subjectBuilder: { content: string }): void {
125
- const importLine = `import { ${subjectNames.join(', ')} } from '../${SubjectRegistration.SUBJECTS_RELATIVE_PATH}/index.js';`;
126
-
127
- subjectBuilder.content = subjectBuilder.content.replace(
128
- /(import type { Test } from '\.\.\/types\/Test\.js';)/,
129
- `$1\n${importLine}`
130
- );
136
+ return subjectDetails;
131
137
  }
138
+ }
132
139
 
133
- private removeSubjectBuilderImports(subjectBuilder: { content: string }): void {
134
- subjectBuilder.content = subjectBuilder.content.replace(SubjectRegistration.IMPORT_MATCH_REGEX, '');
135
- }
140
+ export interface SubjectDetail {
141
+ subject: string;
142
+ variant?: string;
143
+ screen: 'Main' | 'Popup' | 'Zoom' | 'Detail';
136
144
  }
@@ -19,8 +19,10 @@ export class DataRetriever {
19
19
 
20
20
  public getRegisteredSubjects(): IRegisteredSubjects[] | null {
21
21
  const registeredSubjectsPath = path.join(this._consumerRootDirectory, 'seed-data/registeredSubjects.json');
22
+
22
23
  if (!fs.existsSync(registeredSubjectsPath)) {
23
- console.error(`Seed data file not found at: ${registeredSubjectsPath}`);
24
+ console.info(`Seed data file not found at: ${registeredSubjectsPath}`);
25
+
24
26
  return null;
25
27
  }
26
28
 
@@ -31,7 +33,7 @@ export class DataRetriever {
31
33
  public getSubjectsToBuild(): ISubject[] {
32
34
  const subjectsToBuildPath = path.join(this._consumerRootDirectory, 'seed-data/subjectsToBuild.json');
33
35
  if (!fs.existsSync(subjectsToBuildPath)) {
34
- console.error(`subjectsToBuild.json not found at: ${subjectsToBuildPath}. Using all subjects as fallback.`);
36
+ console.info(`subjectsToBuild.json not found at: ${subjectsToBuildPath}. Using all subjects as fallback.`);
35
37
 
36
38
  return this.getSubjectsSeedData();
37
39
  }
@@ -43,7 +45,8 @@ export class DataRetriever {
43
45
  public getScreensToBuild(): string[] {
44
46
  const screensToBuildPath = path.join(this._consumerRootDirectory, 'seed-data/screensToBuild.json');
45
47
  if (!fs.existsSync(screensToBuildPath)) {
46
- console.error(`screensToBuild.json not found at: ${screensToBuildPath}. Using all screens as fallback.`);
48
+ console.info(`screensToBuild.json not found.`);
49
+ return [];
47
50
  }
48
51
 
49
52
  const json = fs.readFileSync(screensToBuildPath, 'utf-8');
@@ -25,7 +25,8 @@ export class NamingHandler {
25
25
  Form: {
26
26
  email: formDefaultSuffix,
27
27
  phone_number: formDefaultSuffix,
28
- suggestion_starts_with: KnownElementTypes.Lookup,
28
+ suggestion_starts_with: formDefaultSuffix,
29
+ lookup_dropdown: KnownElementTypes.Lookup,
29
30
  checkbox: KnownElementTypes.Checkbox,
30
31
  multiline: KnownElementTypes.Textarea,
31
32
  combo: KnownElementTypes.Dropdown
@@ -34,6 +35,7 @@ export class NamingHandler {
34
35
  email: gridDefaultSuffix,
35
36
  phone_number: gridDefaultSuffix,
36
37
  suggestion_starts_with: gridDefaultSuffix,
38
+ lookup_dropdown: gridDefaultSuffix,
37
39
  checkbox: gridDefaultSuffix,
38
40
  multiline: gridDefaultSuffix,
39
41
  combo: gridDefaultSuffix
@@ -46,17 +48,16 @@ export class NamingHandler {
46
48
  ).toLowerCase();
47
49
  }
48
50
 
49
- public getIdFromElementName(elementName: string): string {
50
- return (
51
- elementName
52
- // Remove the suffix at the end
53
- .replace(/([A-Z][a-z0-9]*)$/, '')
54
- // Convert to kebab-case
55
- .replace(/([a-z])([A-Z])/g, '$1-$2')
56
- .toLowerCase()
57
- // Replace underscores with hyphens
58
- .replace(/_/g, '-')
59
- );
51
+ public getIdFromElementName(elementName: string, componentType: SubjectComponents, controlType: string): string {
52
+ const elementSuffixToRemove = this.getElementSuffixFromControlTypeAndComponentType(controlType, componentType);
53
+ const snakeCaseElementName = this.pascalToSnakeCase(elementName);
54
+
55
+ return snakeCaseElementName
56
+ .replace(new RegExp(`${elementSuffixToRemove}$`, 'i'), '')
57
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
58
+ .toLowerCase()
59
+ .replace(/_/g, '-')
60
+ .replace(/-+$/g, '');
60
61
  }
61
62
 
62
63
  public getComponentTypeFromFileName(fileName: string): SubjectComponents {
@@ -71,18 +72,18 @@ export class NamingHandler {
71
72
  }
72
73
 
73
74
  public getElementTypeFromElementName(elementName: string): string {
74
- // Get the suffix at the end of the element name
75
- const match = elementName.match(/([A-Z][a-z0-9]*)$/);
75
+ const snakeCaseElementName = this.pascalToSnakeCase(elementName);
76
+ const parts = snakeCaseElementName.split('_');
76
77
 
77
- // verify that match is a known element type
78
- if (match) {
79
- const elementType = match[1].toLowerCase();
80
- if (Object.values(KnownElementTypes).includes(elementType as KnownElementTypes)) {
81
- return elementType;
78
+ for (let i = parts.length - 1; i > 0; i--) {
79
+ const candidate = parts.slice(i).join('_');
80
+
81
+ if (Object.values(KnownElementTypes).includes(candidate as KnownElementTypes)) {
82
+ return candidate;
82
83
  }
83
84
  }
84
85
 
85
- throw new Error(`Could not extract element type from element name: ${elementName}`);
86
+ throw new Error(`No KnownElementType match found for a partial suffix of "${elementName}"`);
86
87
  }
87
88
 
88
89
  public getSubjectFromFileName(fileName: string): string {
@@ -136,6 +137,13 @@ export class NamingHandler {
136
137
  .join('');
137
138
  }
138
139
 
140
+ public pascalToSnakeCase(value: string): string {
141
+ return value
142
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
143
+ .replace(/([a-z\d])([A-Z])/g, '$1_$2')
144
+ .toLowerCase();
145
+ }
146
+
139
147
  public getComponentIdFromName(componentName: string, componentType: ScreenComponents): string {
140
148
  if (componentType === 'ActionBar' || componentType === 'CustomActionBar') {
141
149
  const capitalizedComponentName = this.capitalizeFirstLetter(componentName);
@@ -0,0 +1,13 @@
1
+ import { performance } from 'node:perf_hooks';
2
+
3
+ export class Stopwatch {
4
+ start(message: string) {
5
+ const startTime = performance.now();
6
+ return {
7
+ stop() {
8
+ const duration = ((performance.now() - startTime) / 1000).toFixed(3);
9
+ console.log(`[${duration}s]: ${message}`);
10
+ }
11
+ };
12
+ }
13
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './DataRetriever.js';
2
2
  export * from './NamingHandler.js';
3
+ export * from './Stopwatch.js';
@@ -3,8 +3,11 @@ import type { IComponentObjects } from '../interfaces/IComponentObjects.js';
3
3
 
4
4
  export abstract class BaseComponentObjects implements IComponentObjects {
5
5
  context: Locator;
6
+ mainHeader: Locator;
6
7
 
7
8
  constructor(page: Page, context: Locator | null = null) {
8
- this.context = context ? context : page.locator('body');
9
+ this.mainHeader = page.locator('main header');
10
+ const body = page.locator('body');
11
+ this.context = context ? context : body;
9
12
  }
10
13
  }
@@ -90,9 +90,9 @@ export class ActionBar extends ActionBarActions {
90
90
  private _actionBarObjects: ActionBarObjects;
91
91
  private _overflowMenu: ActionBarOverflow;
92
92
 
93
- constructor(page: Page, actionbarContext: Locator | null = null) {
94
- super(page, actionbarContext);
95
- this._actionBarObjects = new ActionBarObjects(page, actionbarContext);
93
+ constructor(page: Page, actionBarContext: Locator | null = null) {
94
+ super(page, actionBarContext);
95
+ this._actionBarObjects = new ActionBarObjects(page, actionBarContext);
96
96
  this._overflowMenu = new ActionBarOverflow(page);
97
97
  }
98
98
 
@@ -12,6 +12,10 @@ export class Grid {
12
12
  this._page = page;
13
13
  }
14
14
 
15
+ protected get page() {
16
+ return this._page;
17
+ }
18
+
15
19
  /**
16
20
  * Retrieves a column header element by its text content.
17
21
  * @param text
@@ -253,7 +257,7 @@ export class Grid {
253
257
  const optionButton = this._gridObjects.excelStyleFilterPopupOptionByText(columnId, valueRegex);
254
258
  await optionButton.locator('input[type="checkbox"]').check();
255
259
 
256
- await this._gridObjects.gridCell().first().click();
260
+ await this._gridObjects.mainHeader.click(); // Click outside to close the popup
257
261
  await popup.waitFor({ state: 'detached' });
258
262
  }
259
263
 
@@ -281,11 +285,11 @@ export class Grid {
281
285
  }
282
286
 
283
287
  /**
284
- * Verifies the number of rows in the grid and checks for "No result" overlay if zero.
285
- * @param expectedRowCount The expected number of rows.
288
+ * Verifies the number of records in the grid and checks for "No result" overlay if zero.
289
+ * @param expectedRecordCount The expected number of records.
286
290
  */
287
- public async verifyNumberOfRowsInGrid(expectedRowCount: number): Promise<void> {
288
- if (expectedRowCount === 0) {
291
+ public async verifyGridRecordCount(expectedRecordCount: number): Promise<void> {
292
+ if (expectedRecordCount === 0) {
289
293
  if (await this.hasNoRowsOverlay()) {
290
294
  return;
291
295
  } else {
@@ -294,29 +298,22 @@ export class Grid {
294
298
  }
295
299
 
296
300
  await pollUntil(() => this.rows().count(), {
297
- predicate: (n: number) => n === expectedRowCount
301
+ predicate: (n: number) => n === expectedRecordCount + 1 // +1 to account for header row
298
302
  });
299
303
  }
300
304
 
301
305
  /**
302
306
  * Verifies if the "No result" overlay is visible in the grid.
303
- * @returns A promise that resolves to true if the overlay is visible, otherwise false.
307
+ * @returns A promise that resolves to true if the overlay is visible and , otherwise false.
304
308
  */
305
309
  public async hasNoRowsOverlay(): Promise<boolean> {
306
- try {
307
- const overlay = this._gridObjects.noRowsOverlay();
308
- await pollUntil(
309
- async () => {
310
- const txt = (await overlay.textContent())?.trim() ?? '';
311
- return txt.includes('No result');
312
- },
313
- { predicate: Boolean }
314
- );
315
- } catch {
316
- console.error('The "No result" overlay is not displaying the expected text.');
317
- return false;
318
- }
310
+ const overlay = this._gridObjects.noRowsOverlay();
311
+
312
+ const hasCorrectWording = await pollUntil(
313
+ async () => (await overlay.textContent())?.trim().includes('No result') ?? false,
314
+ { predicate: Boolean }
315
+ );
319
316
 
320
- return await this._gridObjects.noRowsOverlay().isVisible();
317
+ return hasCorrectWording && (await overlay.isVisible());
321
318
  }
322
319
  }
@@ -22,11 +22,11 @@ export class BaseTab {
22
22
  }
23
23
 
24
24
  // --- Useful getters ---
25
- public async getTabById(name: string): Promise<Locator> {
25
+ public getTabById(name: string): Locator {
26
26
  return this._objects.tabByTestId(name);
27
27
  }
28
28
 
29
- public async getTabByIndex(index: number): Promise<Locator> {
29
+ public getTabByIndex(index: number): Locator {
30
30
  return this._objects.tabByIndex(index);
31
31
  }
32
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);
@@ -10,11 +10,11 @@ export class DetailTabPage extends BaseTab {
10
10
  this._objects = new DetailTabPageObjects(page, context);
11
11
  }
12
12
 
13
- public async getDetailTabPageById(name: string): Promise<Locator> {
13
+ public getDetailTabPageById(name: string): Locator {
14
14
  return this._objects.detailTab(name);
15
15
  }
16
16
 
17
- public async getDetailTabPageByIndex(index: number): Promise<Locator> {
17
+ public getDetailTabPageByIndex(index: number): Locator {
18
18
  return this._objects.tabByIndex(index);
19
19
  }
20
20
  }
@@ -19,7 +19,7 @@ export class Tab extends BaseTab {
19
19
  }
20
20
 
21
21
  // Returns the “List” tab locator (scoped).
22
- public async getListTab(tabindex: string = '-1'): Promise<Locator> {
22
+ public getListTab(tabindex: string = '-1'): Locator {
23
23
  return this._objects.listTab(tabindex);
24
24
  }
25
25
 
@@ -29,12 +29,12 @@ export class Tab extends BaseTab {
29
29
  }
30
30
 
31
31
  // Returns the “Form” tab locator (scoped).
32
- public async getFormTab(): Promise<Locator> {
32
+ public getFormTab(): Locator {
33
33
  return this._objects.formTab();
34
34
  }
35
35
 
36
36
  // Opens a component tab by its ID in this tabstrip (scoped).
37
- public async openTabByID(tabid: string): Promise<void> {
38
- await this._objects.tab(tabid).click();
37
+ public async openTabById(tabId: string): Promise<void> {
38
+ await this._objects.tab(tabId).click();
39
39
  }
40
40
  }