@thinkwise/testwise 0.1.97 → 0.2.0-beta.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.
Files changed (199) hide show
  1. package/Testwise.ts +8 -13
  2. package/artifact-builder/ArtifactManager.ts +34 -0
  3. package/artifact-builder/InterfaceGenerator.ts +183 -0
  4. package/artifact-builder/ModelDataBuilder.ts +177 -0
  5. package/artifact-builder/ModelDataRefiner.ts +38 -0
  6. package/artifact-builder/SchemaGenerator.ts +134 -0
  7. package/artifact-builder/ScreenInterfaceRefiner.ts +159 -0
  8. package/artifact-builder/SelectorBuilder.ts +82 -0
  9. package/artifact-builder/SubjectComponentGenerator.ts +186 -0
  10. package/artifact-builder/SubjectGenerator.ts +332 -0
  11. package/artifact-builder/SubjectRegistration.ts +136 -0
  12. package/artifact-builder/helpers/DataRetriever.ts +64 -0
  13. package/artifact-builder/helpers/NamingHandler.ts +180 -0
  14. package/artifact-builder/helpers/index.ts +2 -0
  15. package/artifact-builder/index.ts +9 -0
  16. package/components/{actionbar/Actionbar.ts → action-bar/ActionBar.ts} +12 -12
  17. package/components/{actionbar/ActionbarObjects.ts → action-bar/ActionBarObjects.ts} +4 -4
  18. package/components/{actionbar → action-bar}/CustomActionBar.ts +2 -2
  19. package/components/index.ts +3 -4
  20. package/components/tab/DetailTabPage.ts +20 -0
  21. package/components/tab/{DetailTabObjects.ts → DetailTabPageObjects.ts} +1 -1
  22. package/components/tab/Tab.ts +31 -5
  23. package/components/tab/TabObjects.ts +15 -3
  24. package/dist/Testwise.d.ts +0 -1
  25. package/dist/Testwise.js +7 -14
  26. package/dist/Testwise.js.map +1 -1
  27. package/dist/artifact-builder/ArtifactManager.d.ts +8 -0
  28. package/dist/artifact-builder/ArtifactManager.js +27 -0
  29. package/dist/artifact-builder/ArtifactManager.js.map +1 -0
  30. package/dist/artifact-builder/InterfaceGenerator.d.ts +16 -0
  31. package/dist/artifact-builder/InterfaceGenerator.js +134 -0
  32. package/dist/artifact-builder/InterfaceGenerator.js.map +1 -0
  33. package/dist/artifact-builder/ModelDataBuilder.d.ts +2 -0
  34. package/dist/artifact-builder/ModelDataBuilder.js +128 -0
  35. package/dist/artifact-builder/ModelDataBuilder.js.map +1 -0
  36. package/dist/artifact-builder/ModelDataRefiner.d.ts +4 -0
  37. package/dist/artifact-builder/ModelDataRefiner.js +28 -0
  38. package/dist/artifact-builder/ModelDataRefiner.js.map +1 -0
  39. package/dist/artifact-builder/SchemaGenerator.d.ts +12 -0
  40. package/dist/artifact-builder/SchemaGenerator.js +104 -0
  41. package/dist/artifact-builder/SchemaGenerator.js.map +1 -0
  42. package/dist/artifact-builder/ScreenInterfaceRefiner.d.ts +15 -0
  43. package/dist/artifact-builder/ScreenInterfaceRefiner.js +125 -0
  44. package/dist/artifact-builder/ScreenInterfaceRefiner.js.map +1 -0
  45. package/dist/artifact-builder/SelectorBuilder.d.ts +13 -0
  46. package/dist/artifact-builder/SelectorBuilder.js +69 -0
  47. package/dist/artifact-builder/SelectorBuilder.js.map +1 -0
  48. package/dist/artifact-builder/SubjectComponentGenerator.d.ts +23 -0
  49. package/dist/artifact-builder/SubjectComponentGenerator.js +136 -0
  50. package/dist/artifact-builder/SubjectComponentGenerator.js.map +1 -0
  51. package/dist/artifact-builder/SubjectGenerator.d.ts +27 -0
  52. package/dist/artifact-builder/SubjectGenerator.js +235 -0
  53. package/dist/artifact-builder/SubjectGenerator.js.map +1 -0
  54. package/dist/artifact-builder/SubjectRegistration.d.ts +22 -0
  55. package/dist/artifact-builder/SubjectRegistration.js +96 -0
  56. package/dist/artifact-builder/SubjectRegistration.js.map +1 -0
  57. package/dist/artifact-builder/helpers/DataRetriever.d.ts +12 -0
  58. package/dist/artifact-builder/helpers/DataRetriever.js +52 -0
  59. package/dist/artifact-builder/helpers/DataRetriever.js.map +1 -0
  60. package/dist/artifact-builder/helpers/NamingHandler.d.ts +24 -0
  61. package/dist/artifact-builder/helpers/NamingHandler.js +145 -0
  62. package/dist/artifact-builder/helpers/NamingHandler.js.map +1 -0
  63. package/dist/artifact-builder/helpers/index.d.ts +2 -0
  64. package/dist/artifact-builder/helpers/index.js +3 -0
  65. package/dist/artifact-builder/helpers/index.js.map +1 -0
  66. package/dist/artifact-builder/index.d.ts +9 -0
  67. package/dist/artifact-builder/index.js +10 -0
  68. package/dist/artifact-builder/index.js.map +1 -0
  69. package/dist/components/{actionbar/Actionbar.d.ts → action-bar/ActionBar.d.ts} +4 -4
  70. package/dist/components/{actionbar/Actionbar.js → action-bar/ActionBar.js} +9 -9
  71. package/dist/components/action-bar/ActionBar.js.map +1 -0
  72. package/dist/components/{actionbar/ActionbarObjects.d.ts → action-bar/ActionBarObjects.d.ts} +2 -2
  73. package/dist/components/{actionbar/ActionbarObjects.js → action-bar/ActionBarObjects.js} +5 -5
  74. package/dist/components/action-bar/ActionBarObjects.js.map +1 -0
  75. package/dist/components/{actionbar → action-bar}/CustomActionBar.d.ts +2 -2
  76. package/dist/components/action-bar/CustomActionBar.js +7 -0
  77. package/dist/components/action-bar/CustomActionBar.js.map +1 -0
  78. package/dist/components/index.d.ts +3 -4
  79. package/dist/components/index.js +3 -4
  80. package/dist/components/index.js.map +1 -1
  81. package/dist/components/tab/DetailTabPage.d.ts +9 -0
  82. package/dist/components/tab/DetailTabPage.js +15 -0
  83. package/dist/components/tab/DetailTabPage.js.map +1 -0
  84. package/dist/components/tab/{DetailTabObjects.d.ts → DetailTabPageObjects.d.ts} +1 -1
  85. package/dist/components/tab/{DetailTabObjects.js → DetailTabPageObjects.js} +2 -2
  86. package/dist/components/tab/DetailTabPageObjects.js.map +1 -0
  87. package/dist/components/tab/Tab.d.ts +8 -3
  88. package/dist/components/tab/Tab.js +25 -4
  89. package/dist/components/tab/Tab.js.map +1 -1
  90. package/dist/components/tab/TabObjects.d.ts +7 -2
  91. package/dist/components/tab/TabObjects.js +9 -2
  92. package/dist/components/tab/TabObjects.js.map +1 -1
  93. package/dist/enums/ElementTypes.d.ts +8 -0
  94. package/dist/enums/ElementTypes.js +10 -0
  95. package/dist/enums/ElementTypes.js.map +1 -0
  96. package/dist/helpers/PathResolver.d.ts +3 -0
  97. package/dist/helpers/PathResolver.js +26 -0
  98. package/dist/helpers/PathResolver.js.map +1 -0
  99. package/dist/helpers/index.d.ts +1 -0
  100. package/dist/helpers/index.js +1 -0
  101. package/dist/helpers/index.js.map +1 -1
  102. package/dist/index.d.ts +1 -0
  103. package/dist/index.js +1 -0
  104. package/dist/index.js.map +1 -1
  105. package/dist/interfaces/IProperty.d.ts +4 -0
  106. package/dist/interfaces/IProperty.js +2 -0
  107. package/dist/interfaces/IProperty.js.map +1 -0
  108. package/dist/interfaces/IRegisteredSubjects.d.ts +5 -0
  109. package/dist/interfaces/IRegisteredSubjects.js +2 -0
  110. package/dist/interfaces/IRegisteredSubjects.js.map +1 -0
  111. package/dist/interfaces/ISubject.d.ts +8 -0
  112. package/dist/interfaces/ISubject.js +2 -0
  113. package/dist/interfaces/ISubject.js.map +1 -0
  114. package/dist/page-extensions/SubjectProvider.d.ts +11 -0
  115. package/dist/page-extensions/SubjectProvider.js +24 -0
  116. package/dist/page-extensions/SubjectProvider.js.map +1 -0
  117. package/dist/page-extensions/SubjectRegistry.d.ts +14 -0
  118. package/dist/page-extensions/SubjectRegistry.js +14 -0
  119. package/dist/page-extensions/SubjectRegistry.js.map +1 -0
  120. package/dist/page-extensions/index.d.ts +3 -0
  121. package/dist/page-extensions/index.js +3 -0
  122. package/dist/page-extensions/index.js.map +1 -1
  123. package/dist/services/ConfigBuilder.d.ts +1 -0
  124. package/dist/services/ConfigBuilder.js +20 -1
  125. package/dist/services/ConfigBuilder.js.map +1 -1
  126. package/dist/test-artifacts/SubjectPageBase.d.ts +5 -0
  127. package/dist/test-artifacts/SubjectPageBase.js +6 -0
  128. package/dist/test-artifacts/SubjectPageBase.js.map +1 -0
  129. package/dist/test-artifacts/index.d.ts +3 -0
  130. package/dist/test-artifacts/index.js +4 -0
  131. package/dist/test-artifacts/index.js.map +1 -0
  132. package/dist/test-artifacts/screens/index.d.ts +1 -0
  133. package/dist/test-artifacts/screens/index.js +2 -0
  134. package/dist/test-artifacts/screens/index.js.map +1 -0
  135. package/dist/test-artifacts/subjects/index.d.ts +1 -0
  136. package/dist/test-artifacts/subjects/index.js +2 -0
  137. package/dist/test-artifacts/subjects/index.js.map +1 -0
  138. package/dist/types/Components.d.ts +7 -0
  139. package/dist/types/Components.js +28 -0
  140. package/dist/types/Components.js.map +1 -0
  141. package/enums/ElementTypes.ts +8 -0
  142. package/helpers/PathResolver.ts +30 -0
  143. package/helpers/index.ts +1 -0
  144. package/index.ts +1 -0
  145. package/interfaces/IProperty.ts +4 -0
  146. package/interfaces/IRegisteredSubjects.ts +5 -0
  147. package/interfaces/ISubject.ts +9 -0
  148. package/package.json +26 -9
  149. package/page-extensions/SubjectProvider.ts +41 -0
  150. package/page-extensions/SubjectRegistry.ts +30 -0
  151. package/page-extensions/index.ts +3 -0
  152. package/promptCredentials.js +124 -124
  153. package/scripts/Testwise.template.json +4 -1
  154. package/scripts/main.js +75 -4
  155. package/scripts/postinstall.js +42 -0
  156. package/scripts/setup.js +17 -14
  157. package/scripts/sync.js +69 -0
  158. package/scripts/tsconfig.template.json +1 -1
  159. package/services/ConfigBuilder.ts +25 -2
  160. package/test-artifacts/SubjectPageBase.ts +9 -0
  161. package/test-artifacts/index.ts +3 -0
  162. package/test-artifacts/screens/index.ts +0 -0
  163. package/test-artifacts/subjects/index.ts +0 -0
  164. package/tsconfig.json +1 -1
  165. package/types/Components.ts +55 -0
  166. package/components/tab/ComponentTab.ts +0 -40
  167. package/components/tab/ComponentTabObjects.ts +0 -17
  168. package/components/tab/DetailTab.ts +0 -20
  169. package/dist/Testwise.json +0 -25
  170. package/dist/bdd.d.ts +0 -6
  171. package/dist/bdd.js +0 -9
  172. package/dist/bdd.js.map +0 -1
  173. package/dist/biome.json +0 -52
  174. package/dist/components/actionbar/Actionbar.js.map +0 -1
  175. package/dist/components/actionbar/ActionbarObjects.js.map +0 -1
  176. package/dist/components/actionbar/CustomActionBar.js +0 -7
  177. package/dist/components/actionbar/CustomActionBar.js.map +0 -1
  178. package/dist/components/tab/ComponentTab.d.ts +0 -12
  179. package/dist/components/tab/ComponentTab.js +0 -31
  180. package/dist/components/tab/ComponentTab.js.map +0 -1
  181. package/dist/components/tab/ComponentTabObjects.d.ts +0 -8
  182. package/dist/components/tab/ComponentTabObjects.js +0 -11
  183. package/dist/components/tab/ComponentTabObjects.js.map +0 -1
  184. package/dist/components/tab/DetailTab.d.ts +0 -9
  185. package/dist/components/tab/DetailTab.js +0 -15
  186. package/dist/components/tab/DetailTab.js.map +0 -1
  187. package/dist/components/tab/DetailTabObjects.js.map +0 -1
  188. package/dist/helpers/TestExtensions.d.ts +0 -8
  189. package/dist/helpers/TestExtensions.js +0 -21
  190. package/dist/helpers/TestExtensions.js.map +0 -1
  191. package/dist/package-lock.json +0 -3852
  192. package/dist/package.json +0 -59
  193. package/dist/scripts/Testwise.template.json +0 -25
  194. package/dist/scripts/tsconfig.template.json +0 -12
  195. package/dist/services/ReportingService.d.ts +0 -8
  196. package/dist/services/ReportingService.js +0 -29
  197. package/dist/services/ReportingService.js.map +0 -1
  198. package/dist/tsconfig.json +0 -20
  199. package/services/ReportingService.ts +0 -37
@@ -0,0 +1,332 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { DataRetriever, NamingHandler } from '../index.js';
6
+ import type { ISubject } from '../interfaces/ISubject.js';
7
+ import {
8
+ coreComponents,
9
+ type ScreenComponents,
10
+ type SubjectAgnosticComponents,
11
+ screenComponents
12
+ } from '../types/Components.js';
13
+ import { type LocatorType, SelectorBuilder } from './SelectorBuilder.js';
14
+
15
+ export class SubjectGenerator {
16
+ private static readonly SUBJECTS_RELATIVE_PATH = 'test-artifacts/subjects';
17
+
18
+ private readonly _currentFilename: string;
19
+ private readonly _currentDirname: string;
20
+ private readonly _subjectsDirectory: string;
21
+ private readonly _namingHandler: NamingHandler;
22
+ private readonly _selectorBuilder: SelectorBuilder;
23
+
24
+ constructor() {
25
+ this._currentFilename = fileURLToPath(import.meta.url);
26
+ this._currentDirname = dirname(this._currentFilename);
27
+ this._subjectsDirectory = path.resolve(this._currentDirname, `../../${SubjectGenerator.SUBJECTS_RELATIVE_PATH}`);
28
+ this._namingHandler = new NamingHandler();
29
+ this._selectorBuilder = new SelectorBuilder();
30
+ }
31
+
32
+ public run(): void {
33
+ this.generateSubjectClasses();
34
+ this.indexGeneratedSubjects();
35
+ }
36
+
37
+ private generateSubjectClasses(): void {
38
+ const subjects: ISubject[] = new DataRetriever().getSubjectsToBuild();
39
+
40
+ subjects.forEach((subject) => {
41
+ this.createSubjectClass(subject);
42
+ });
43
+ }
44
+
45
+ private getAllSubjectFolders(): string[] {
46
+ return fs.readdirSync(this._subjectsDirectory).filter((file) => {
47
+ return fs.statSync(path.join(this._subjectsDirectory, file)).isDirectory();
48
+ });
49
+ }
50
+
51
+ private indexGeneratedSubjects(): void {
52
+ const subjectFolders = this.getAllSubjectFolders();
53
+
54
+ subjectFolders.forEach((folder) => {
55
+ const exportLines: string[] = [];
56
+ const subjectFolderPath = path.join(this._subjectsDirectory, folder);
57
+ const tsFiles = fs.readdirSync(subjectFolderPath).filter((file) => file.endsWith('.ts') && file !== 'index.ts');
58
+ const indexFile = path.join(subjectFolderPath, 'index.ts');
59
+
60
+ if(tsFiles.length === 0) return;
61
+
62
+ if (!fs.existsSync(indexFile)) {
63
+ fs.writeFileSync(indexFile, '', 'utf-8');
64
+ }
65
+
66
+ tsFiles.forEach((tsFile) => {
67
+ if (tsFile === 'index.ts') return;
68
+
69
+ const className = tsFile.replace('.ts', '');
70
+ exportLines.push(`export { ${className} } from './${className}.js';`);
71
+ });
72
+
73
+ fs.writeFileSync(indexFile, exportLines.join('\n'), 'utf-8');
74
+ });
75
+ }
76
+
77
+ private createSubjectClass(subject: ISubject): void {
78
+ const screenInterfaceName = this._namingHandler.generateScreenInterfaceName(subject.screentype_id);
79
+ const screenInterfacePath = this.getScreenInterfacePath(screenInterfaceName);
80
+ const screenFileContent = fs.readFileSync(screenInterfacePath, 'utf-8');
81
+ const subjectFolderName = this._namingHandler.formatPascalCaseName(subject.subject);
82
+ const subjectFolderPath = this.getSubjectFolderRelativePath(subjectFolderName);
83
+
84
+ const subjectClass = {
85
+ content: `import type { Page } from '@playwright/test';\nimport { SubjectPageBase } from '../../index.js';\n`,
86
+ name: this._namingHandler.generateSubjectClassName(subject)
87
+ };
88
+ const componentDictionary: { name: string; type: SubjectAgnosticComponents; context: string }[] =
89
+ this.getAllComponentsFromInterface(screenFileContent, screenInterfaceName, subject) as {
90
+ name: string;
91
+ type: SubjectAgnosticComponents;
92
+ context: string;
93
+ }[];
94
+
95
+ this.addScreenInterfaceImport(subjectClass, screenInterfaceName);
96
+ this.removeComponentImports(subjectClass);
97
+ this.addCoreComponentImports(componentDictionary, subjectFolderName, subjectClass);
98
+ this.addClassExport(subjectClass, screenInterfaceName);
99
+ this.addComponentDeclarations(subjectClass, componentDictionary, subjectFolderName);
100
+ this.addConstructor(subjectClass, componentDictionary, subjectFolderName, subject);
101
+ this.addSubjectClassLineEndings(subjectClass);
102
+ this.verifyDirectoryExists(subjectFolderPath);
103
+ fs.writeFileSync(path.join(subjectFolderPath, `${subjectClass.name}.ts`), subjectClass.content, 'utf-8');
104
+ }
105
+
106
+ private getAllComponentsFromInterface(
107
+ screenFileContent: string,
108
+ screenInterfaceName: string,
109
+ subject: ISubject
110
+ ): { name: string; type: string; context: string }[] {
111
+ let componentDictionary: { name: string; type: string; context: string }[] = [];
112
+
113
+ const interfaceMatches = screenFileContent.match(
114
+ new RegExp(`export interface ${screenInterfaceName} {([\\s\\S]*?)}`)
115
+ );
116
+
117
+ if (interfaceMatches) {
118
+ const propertyMatches = interfaceMatches[1].match(/^\s*([a-zA-Z0-9_]+)\s*:\s*([a-zA-Z0-9_]+);/gm);
119
+ if (propertyMatches) {
120
+ componentDictionary = propertyMatches.map((line) => {
121
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
122
+ const [_, name, type] = line.match(/^\s*([a-zA-Z0-9_]+)\s*:\s*([a-zA-Z0-9_]+);/) || [];
123
+ const context = this.generateComponentContextLocatorString(type as ScreenComponents, name, subject);
124
+
125
+ return { name, type, context };
126
+ });
127
+ }
128
+ }
129
+
130
+ return componentDictionary;
131
+ }
132
+
133
+ // ToDo: decide if this needs to move into a ContextGenerator class (depending on the complexity of context generation)
134
+ private generateComponentContextLocatorString(
135
+ componentType: ScreenComponents,
136
+ componentName: string,
137
+ subject: ISubject
138
+ ): string {
139
+ const componentId = this._namingHandler.getComponentIdFromName(componentName, componentType);
140
+ const screentypeId = subject.screentype_id.replace(/_/g, '-');
141
+ const locatorString = {
142
+ prefix: '',
143
+ suffix: '',
144
+ locatorType: 'getByTestId' as LocatorType
145
+ };
146
+
147
+ switch (componentType) {
148
+ case 'Form':
149
+ case 'Grid':
150
+ case 'Splitter':
151
+ case 'FilterForm':
152
+ case 'Scheduler':
153
+ locatorString.prefix = `screen__${screentypeId}__`;
154
+ break;
155
+ case 'ActionBar':
156
+ case 'CustomActionBar':
157
+ locatorString.prefix = '.';
158
+ locatorString.locatorType = 'locator';
159
+ break;
160
+ case 'Tab':
161
+ locatorString.prefix = '.Tab.';
162
+ locatorString.locatorType = 'locator';
163
+ break;
164
+ case 'DetailTabPage':
165
+ return `this.context`;
166
+ }
167
+
168
+ return `this.context.${locatorString.locatorType}('${locatorString.prefix}${componentId}${locatorString.suffix}')`;
169
+ }
170
+
171
+ private verifyDirectoryExists(directoryPath: string): void {
172
+ if (!fs.existsSync(directoryPath)) {
173
+ fs.mkdirSync(directoryPath, { recursive: true });
174
+ }
175
+ }
176
+
177
+ private getScreenInterfacePath(screenInterfaceName: string): string {
178
+ const screenInterfacePath = path.resolve(
179
+ this._currentDirname,
180
+ `../../test-artifacts/screens/${screenInterfaceName}.ts`
181
+ );
182
+
183
+ if (!fs.existsSync(screenInterfacePath)) {
184
+ throw new Error(`Screen interface file not found: ${screenInterfacePath}`);
185
+ }
186
+
187
+ return screenInterfacePath;
188
+ }
189
+
190
+ private getSubjectFolderRelativePath(subjectFolderName: string): string {
191
+ const subjectFolderPath = path.resolve(
192
+ this._currentDirname,
193
+ `../../${SubjectGenerator.SUBJECTS_RELATIVE_PATH}/${subjectFolderName}`
194
+ );
195
+ return subjectFolderPath;
196
+ }
197
+
198
+ private addCoreComponentImports(
199
+ componentDictionary: { name: string; type: SubjectAgnosticComponents }[],
200
+ subjectFolderName: string,
201
+ subjectClass: { content: string }
202
+ ): void {
203
+ const coreComponentImports = new Set<string>();
204
+
205
+ componentDictionary.forEach((component) => {
206
+ if (!screenComponents.includes(component.type)) return;
207
+
208
+ if (coreComponents.includes(component.type)) {
209
+ const importRegex = new RegExp(
210
+ `import \\{[^}]*\\b${component.type}\\b[^}]*\\} from '../../../components/index.js';`
211
+ );
212
+
213
+ if (importRegex.test(subjectClass.content)) {
214
+ return;
215
+ }
216
+
217
+ coreComponentImports.add(component.type);
218
+ return;
219
+ }
220
+
221
+ const componentName = component.type;
222
+ const componentImport = `import { type ${subjectFolderName}${componentName}, ${subjectFolderName}${componentName}Component } from './Components/${subjectFolderName}${componentName}.js';`;
223
+
224
+ if (!subjectClass.content.includes(componentImport)) {
225
+ subjectClass.content = `${componentImport}\n${subjectClass.content}`;
226
+ }
227
+ });
228
+
229
+ if (coreComponentImports.size > 0) {
230
+ subjectClass.content = `import { ${Array.from(coreComponentImports).join(', ')} } from '../../../components/index.js';\n${subjectClass.content}`;
231
+ }
232
+ }
233
+
234
+ private removeComponentImports(subjectClass: { content: string }): void {
235
+ subjectClass.content = subjectClass.content.replace(
236
+ /import \{[^}]+\} from '\.\.\/\.\.\/components\/index\.js';\n?/g,
237
+ ''
238
+ );
239
+ }
240
+
241
+ private addClassExport(subjectClass: { content: string; name: string }, screenInterfaceName: string): void {
242
+ subjectClass.content += `\nexport class ${subjectClass.name} extends SubjectPageBase implements ${screenInterfaceName} {\n`;
243
+ }
244
+
245
+ private addScreenInterfaceImport(subjectClass: { content: string }, screenInterfaceName: string): void {
246
+ if (subjectClass.content.includes(`import type { ${screenInterfaceName} }`)) {
247
+ return;
248
+ }
249
+
250
+ subjectClass.content = `import type { ${screenInterfaceName} } from '../../screens/index.js';\n${subjectClass.content}`;
251
+ }
252
+
253
+ private addComponentDeclarations(
254
+ subjectClass: { content: string },
255
+ componentDictionary: { name: string; type: SubjectAgnosticComponents }[],
256
+ subjectFolderName: string
257
+ ): void {
258
+ componentDictionary.forEach((component) => {
259
+ if (!screenComponents.includes(component.type)) return;
260
+
261
+ const propertyName = this._namingHandler.formatPropertyName(component.name);
262
+ if (coreComponents.includes(component.type)) {
263
+ subjectClass.content += ` ${propertyName}: ${component.type};\n`;
264
+ return;
265
+ }
266
+ const componentType = `${subjectFolderName}${component.type}`;
267
+ subjectClass.content += ` ${propertyName}: ${componentType};\n`;
268
+ });
269
+ }
270
+
271
+ private addConstructor(
272
+ subjectClass: { content: string },
273
+ componentDictionary: { name: string; type: SubjectAgnosticComponents; context: string }[],
274
+ subjectFolderName: string,
275
+ subject: ISubject
276
+ ): void {
277
+ const rootContextSelector = this._selectorBuilder.getPageContext(subject);
278
+
279
+ subjectClass.content += `\n constructor(page: Page) {\n super(page.locator('${rootContextSelector}'));\n`;
280
+
281
+ componentDictionary.forEach((component) => {
282
+ const propertyName = this._namingHandler.formatPropertyName(component.name);
283
+
284
+ if (!screenComponents.includes(component.type)) return;
285
+
286
+ if (coreComponents.includes(component.type)) {
287
+ subjectClass.content += ` this.${propertyName} = ${this.getComponentInitializer(component)};\n`;
288
+ return;
289
+ }
290
+
291
+ const componentName = component.type;
292
+ subjectClass.content += ` this.${propertyName} = new ${subjectFolderName}${componentName}Component(page, ${component.context});\n`;
293
+ });
294
+ }
295
+
296
+ private getComponentInitializer(component: { type: string; context: string }): string {
297
+ const componentNamePascal = this._namingHandler.formatPascalCaseName(component.type);
298
+
299
+ switch (component.type) {
300
+ case 'Cardlist':
301
+ case 'TaskTiles':
302
+ case 'PrefilterList':
303
+ case 'TaskBar':
304
+ case 'Chart':
305
+ case 'CubeViewBar':
306
+ case 'PivotGrid':
307
+ case 'PivotGridFieldList':
308
+ case 'Maps':
309
+ case 'ReportBar':
310
+ case 'ReportTiles':
311
+ case 'Preview':
312
+ return `new ${component.type}(page, this.context.locator('.${component.type}.${componentNamePascal}'))`;
313
+ case 'CombinedFilter':
314
+ case 'CustomActionBar':
315
+ case 'ActionBar':
316
+ case 'Splitter':
317
+ case 'DetailTabPage':
318
+ case 'Tab':
319
+ case 'FilterForm':
320
+ case 'Scheduler':
321
+ return `new ${component.type}(page, ${component.context})`;
322
+ case 'TreeView':
323
+ return `new ${component.type}(page, this.context.locator('.Tree.${componentNamePascal}'))`;
324
+ default:
325
+ throw new Error(`Unknown core component type: ${component.type}`);
326
+ }
327
+ }
328
+
329
+ private addSubjectClassLineEndings(subjectClass: { content: string }): void {
330
+ subjectClass.content += ` }\n}\n`;
331
+ }
332
+ }
@@ -0,0 +1,136 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ export class SubjectRegistration {
7
+ private static readonly SUBJECTS_RELATIVE_PATH = 'test-artifacts/subjects';
8
+ private static readonly IMPORT_MATCH_REGEX = /import .*'\.\.\/test-artifacts\/subjects\/index\.js';\n?/g;
9
+
10
+ private readonly _currentFilename: string;
11
+ private readonly _currentDirname: string;
12
+ private readonly _subjectTypePath: string;
13
+ private readonly _subjectsIndexDirectory: string;
14
+ private readonly _subjectBuilderPath: string;
15
+
16
+ constructor() {
17
+ this._currentFilename = fileURLToPath(import.meta.url);
18
+ this._currentDirname = dirname(this._currentFilename);
19
+ 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
+ }
26
+
27
+ public run(): void {
28
+ this.registerGeneratedSubjectsAsSubjectTypes();
29
+ this.registerGeneratedSubjectsUnderPageGet();
30
+ }
31
+
32
+ private registerGeneratedSubjectsAsSubjectTypes(): void {
33
+ const subjectList = this.getAllSubjects();
34
+ const subjectTypeImportsAndExports = this.getSubjectTypesImportsAndExports(subjectList);
35
+ const subject = { content: this.setSubjectTypeContent() };
36
+
37
+ this.removeExistingSubjectTypeDefinition(subject);
38
+ this.removeAllImportsFromSubjectIndex(subject);
39
+
40
+ 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') };
47
+
48
+ this.removeSubjectBuilderImports(subjectBuilder);
49
+ this.removeSubjectBuilderSubjectDefinition(subjectBuilder);
50
+ this.addSubjectBuilderImports(subjectNames, subjectBuilder);
51
+ this.addSubjectBuilderSubjectDefinition(subjectNames, subjectBuilder);
52
+
53
+ fs.writeFileSync(this._subjectBuilderPath, subjectBuilder.content, 'utf-8');
54
+ }
55
+
56
+ private removeExistingSubjectTypeDefinition(subject: { content: string }): void {
57
+ const subjectDefinitionMatchRegex: RegExp = /export\s+type\s+SubjectType\s*=\s*{[^}]*};?/m;
58
+
59
+ if (subject.content) {
60
+ subject.content = subject.content.replace(subjectDefinitionMatchRegex, '');
61
+ }
62
+ }
63
+
64
+ private setSubjectTypeContent(): string {
65
+ return fs.readFileSync(this._subjectTypePath, 'utf-8');
66
+ }
67
+
68
+ private removeAllImportsFromSubjectIndex(subject: { content: string }): void {
69
+ if (subject.content) {
70
+ subject.content = subject.content.replace(SubjectRegistration.IMPORT_MATCH_REGEX, '');
71
+ }
72
+ }
73
+
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 = {
80
+ ${subjectTypeEntries}
81
+ };
82
+ `;
83
+
84
+ return newSubjectTypes;
85
+ }
86
+
87
+ private getAllSubjects(): string[] {
88
+ const subjectsDir = path.resolve(this._currentDirname, '../../test-artifacts/subjects');
89
+
90
+ const subjectFolders = fs.readdirSync(subjectsDir).filter((file) => {
91
+ return fs.statSync(path.join(subjectsDir, file)).isDirectory();
92
+ });
93
+
94
+ const subjectNames: string[] = [];
95
+
96
+ subjectFolders.forEach((folder) => {
97
+ const folderPath = path.join(subjectsDir, folder);
98
+ const tsFiles = fs.readdirSync(folderPath).filter((file) => file.endsWith('.ts'));
99
+
100
+ tsFiles.forEach((tsFile) => {
101
+ const classContent = fs.readFileSync(path.join(folderPath, tsFile), 'utf-8');
102
+ const classMatches = classContent.matchAll(/export\s+class\s+(\w+)/g);
103
+ for (const match of classMatches) {
104
+ subjectNames.push(match[1]);
105
+ }
106
+ });
107
+ });
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
+ );
131
+ }
132
+
133
+ private removeSubjectBuilderImports(subjectBuilder: { content: string }): void {
134
+ subjectBuilder.content = subjectBuilder.content.replace(SubjectRegistration.IMPORT_MATCH_REGEX, '');
135
+ }
136
+ }
@@ -0,0 +1,64 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { PathResolver } from '../../helpers/PathResolver.js';
4
+ import type { IRegisteredSubjects } from '../../interfaces/IRegisteredSubjects.js';
5
+ import type { ISubject } from '../../interfaces/ISubject.js';
6
+
7
+ export class DataRetriever {
8
+ private _consumerRootDirectory: string = new PathResolver().getConsumerRootDirectory();
9
+
10
+ public getSubjectsSeedData(): ISubject[] {
11
+ const seedPath = path.join(this._consumerRootDirectory, 'seed-data/subjects.json');
12
+ if (!fs.existsSync(seedPath)) {
13
+ console.error(`Seed data file not found at: ${seedPath}`);
14
+ }
15
+
16
+ const json = fs.readFileSync(seedPath, 'utf-8');
17
+ return this.toISubject(json) as unknown as ISubject[];
18
+ }
19
+
20
+ public getRegisteredSubjects(): IRegisteredSubjects[] | null {
21
+ const registeredSubjectsPath = path.join(this._consumerRootDirectory, 'seed-data/registeredSubjects.json');
22
+ if (!fs.existsSync(registeredSubjectsPath)) {
23
+ console.error(`Seed data file not found at: ${registeredSubjectsPath}`);
24
+ return null;
25
+ }
26
+
27
+ const json = fs.readFileSync(registeredSubjectsPath, 'utf-8');
28
+ return this.toIRegisteredSubjects(json) as unknown as IRegisteredSubjects[];
29
+ }
30
+
31
+ public getSubjectsToBuild(): ISubject[] {
32
+ const subjectsToBuildPath = path.join(this._consumerRootDirectory, 'seed-data/subjectsToBuild.json');
33
+ if (!fs.existsSync(subjectsToBuildPath)) {
34
+ console.error(`subjectsToBuild.json not found at: ${subjectsToBuildPath}. Using all subjects as fallback.`);
35
+
36
+ return this.getSubjectsSeedData();
37
+ }
38
+
39
+ const json = fs.readFileSync(subjectsToBuildPath, 'utf-8');
40
+ return this.toISubject(json) as unknown as ISubject[];
41
+ }
42
+
43
+ public getScreensToBuild(): string[] {
44
+ const screensToBuildPath = path.join(this._consumerRootDirectory, 'seed-data/screensToBuild.json');
45
+ if (!fs.existsSync(screensToBuildPath)) {
46
+ console.error(`screensToBuild.json not found at: ${screensToBuildPath}. Using all screens as fallback.`);
47
+ }
48
+
49
+ const json = fs.readFileSync(screensToBuildPath, 'utf-8');
50
+ return JSON.parse(json) as string[];
51
+ }
52
+
53
+ public getSeedDataDirectory(): string {
54
+ return path.join(this._consumerRootDirectory, 'seed-data');
55
+ }
56
+
57
+ private toISubject(json: string): ISubject {
58
+ return JSON.parse(json);
59
+ }
60
+
61
+ private toIRegisteredSubjects(json: string): IRegisteredSubjects {
62
+ return JSON.parse(json);
63
+ }
64
+ }