@thinkwise/testwise 0.1.97 → 0.2.0-beta.3

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,159 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { disabledScreenComponents, type ScreenComponents, screenComponents } from '../types/Components.js';
5
+
6
+ export class ScreenInterfaceRefiner {
7
+ private readonly _screensDirectory: string;
8
+
9
+ constructor() {
10
+ const _filename = fileURLToPath(import.meta.url);
11
+ const _dirname = path.dirname(_filename);
12
+ this._screensDirectory = path.resolve(_dirname, '../../test-artifacts/screens');
13
+ }
14
+
15
+ public refine() {
16
+ console.log('Refining interfaces in:', this._screensDirectory);
17
+
18
+ const screenFiles = this.getAllScreenInterfaceFiles();
19
+
20
+ screenFiles.forEach((screenFile) => {
21
+ const filePath = path.join(this._screensDirectory, screenFile);
22
+ const screen = { content: fs.readFileSync(filePath, 'utf-8') };
23
+
24
+ const interfaces = this.findInterfaces(screen.content);
25
+
26
+ this.removeUnsupportedComponents(screen);
27
+ this.consolidateTabComponents(screen);
28
+ this.removeEmptyLines(screen);
29
+
30
+ if (interfaces.length > 1) {
31
+ const interfacesToRemove = this.getPlaceholderInterfaces(screen, interfaces);
32
+
33
+ this.addImportStatements(screen, interfacesToRemove);
34
+ this.formatContent(screen);
35
+
36
+ fs.writeFileSync(filePath, screen.content, 'utf-8');
37
+ console.log(`Refined interfaces in: ${screenFile}`);
38
+ } else {
39
+ console.log(`No additional interfaces to refine in: ${screenFile}`);
40
+ }
41
+
42
+ this.indexScreenInterface(screenFile);
43
+ });
44
+ }
45
+
46
+ private removeEmptyLines(screen: { content: string }) {
47
+ screen.content = screen.content.replace(/^\s*[\r\n]/gm, '');
48
+ }
49
+
50
+ private indexScreenInterface(screenFile: string) {
51
+ const indexPath = path.resolve(this._screensDirectory, 'index.ts');
52
+ let indexContent = fs.readFileSync(indexPath, 'utf-8');
53
+
54
+ const fileJS = screenFile.replace('.ts', '.js');
55
+ const exportLine = `export * from './${fileJS}';\n`;
56
+ if (!indexContent.includes(exportLine)) {
57
+ const lines = indexContent.split('\n');
58
+ let insertPos = 0;
59
+ while (insertPos < lines.length && lines[insertPos].startsWith('export')) {
60
+ insertPos++;
61
+ }
62
+ lines.splice(insertPos, 0, exportLine.trim());
63
+ indexContent = lines.join('\n');
64
+ fs.writeFileSync(indexPath, indexContent, 'utf-8');
65
+ }
66
+ }
67
+
68
+ private formatContent(screen: { content: string }) {
69
+ screen.content = screen.content.replace(/(\n\s*)+$/g, '\n');
70
+ }
71
+
72
+ private addImportStatements(screen: { content: string }, interfacesToRemoveList: string[]) {
73
+ const importsToAdd = interfacesToRemoveList.filter(
74
+ (interfaceName) =>
75
+ (Object.values(screenComponents) as string[]).includes(interfaceName) &&
76
+ !(Object.values(disabledScreenComponents) as string[]).includes(interfaceName)
77
+ );
78
+
79
+ const importStatement = `import type { ${importsToAdd.join(', ')} } from '../../components/index.js';\n\n`;
80
+ screen.content = importStatement + screen.content;
81
+ }
82
+
83
+ private isActiveScreenComponent(value: string): value is ScreenComponents {
84
+ if (disabledScreenComponents.includes(value as ScreenComponents)) {
85
+ return false;
86
+ }
87
+
88
+ return screenComponents.includes(value as ScreenComponents);
89
+ }
90
+
91
+ private removeUnsupportedComponents(screen: { content: string }) {
92
+ const propertyRegex = /(\s+)(\w+): (\w+);/g;
93
+ screen.content = screen.content.replace(propertyRegex, (match, _p1, _p2, p3) => {
94
+ if (this.isActiveScreenComponent(p3)) {
95
+ return match;
96
+ } else {
97
+ return '';
98
+ }
99
+ });
100
+ }
101
+
102
+ // This is a short-term solution until we implement a more robust tab consolidation mechanism
103
+ private consolidateTabComponents(screen: { content: string }) {
104
+ let hasTab = false;
105
+ let hasDetailTabPage = false;
106
+
107
+ screen.content = screen.content.replace(
108
+ /^[ \t]*([a-zA-Z0-9_]+)[ \t]*:[ \t]*(Tab|DetailTabPage)[ \t]*;[ \t]*$/gim,
109
+ (_match, _prop, type) => {
110
+ switch (type) {
111
+ case 'Tab':
112
+ hasTab = true;
113
+ break;
114
+ case 'DetailTabPage':
115
+ hasDetailTabPage = true;
116
+ break;
117
+ }
118
+
119
+ return '';
120
+ }
121
+ );
122
+
123
+ let insert = '';
124
+
125
+ if (hasTab) insert += ' tab: Tab;\n';
126
+ if (hasDetailTabPage) insert += ' detailTab: DetailTabPage;\n';
127
+ if (insert) {
128
+ screen.content = screen.content.replace(/([ \t]*)}/, (_m, ws) => `${insert}${ws}}`);
129
+ }
130
+ }
131
+
132
+ private getPlaceholderInterfaces(screen: { content: string }, interfaces: string[]): string[] {
133
+ const interfacesToRemove = interfaces.slice(1);
134
+
135
+ interfacesToRemove.forEach((interfaceName) => {
136
+ const interfaceRegex = new RegExp(`export interface ${interfaceName} \\{[\\s\\S]*?\\}`, 'g');
137
+ screen.content = screen.content.replace(interfaceRegex, '');
138
+ });
139
+
140
+ return interfacesToRemove;
141
+ }
142
+
143
+ private findInterfaces(content: string): string[] {
144
+ const interfaceRegex = /export interface (\w+) \{[\s\S]*?\}/g;
145
+ const interfaces = [];
146
+ let match = interfaceRegex.exec(content);
147
+
148
+ while (match !== null) {
149
+ interfaces.push(match[1]);
150
+ match = interfaceRegex.exec(content);
151
+ }
152
+
153
+ return interfaces;
154
+ }
155
+
156
+ private getAllScreenInterfaceFiles(): string[] {
157
+ return fs.readdirSync(this._screensDirectory).filter((f) => f.endsWith('.ts'));
158
+ }
159
+ }
@@ -0,0 +1,82 @@
1
+ import { KnownElementTypes } from '../enums/ElementTypes.js';
2
+ import type { ISubject } from '../interfaces/ISubject.js';
3
+ import type { SubjectComponents } from '../types/Components.js';
4
+ import { NamingHandler } from './helpers/index.js';
5
+
6
+ export class SelectorBuilder {
7
+ private _namingHandler: NamingHandler;
8
+
9
+ constructor() {
10
+ this._namingHandler = new NamingHandler();
11
+ }
12
+
13
+ private determineSelectorSuffix(property: string, componentName: SubjectComponents): string | null {
14
+ let selectorSuffix = null;
15
+
16
+ if (componentName === 'Form') {
17
+ if (this._namingHandler.getElementTypeFromElementName(property) === KnownElementTypes.Lookup) {
18
+ selectorSuffix = '__control__input';
19
+ } else if (this._namingHandler.getElementTypeFromElementName(property) === KnownElementTypes.Dropdown) {
20
+ selectorSuffix = '__select__input';
21
+ }
22
+ }
23
+
24
+ return selectorSuffix;
25
+ }
26
+
27
+ private getDefaultPrefix(componentName: SubjectComponents): string {
28
+ let prefix = '';
29
+
30
+ if (componentName === 'Grid') {
31
+ prefix = '[col-id="';
32
+ } else if (componentName === 'Form') {
33
+ prefix = 'form-field__';
34
+ } else {
35
+ throw new Error(`Unsupported component type: ${componentName}`);
36
+ }
37
+
38
+ return prefix;
39
+ }
40
+
41
+ private getDefaultSuffix(componentName: SubjectComponents): string {
42
+ let suffix = '';
43
+
44
+ if (componentName === 'Grid') {
45
+ suffix = '"]';
46
+ } else if (componentName === 'Form') {
47
+ suffix = '__input';
48
+ } else {
49
+ throw new Error(`Unsupported component type: ${componentName}`);
50
+ }
51
+
52
+ return suffix;
53
+ }
54
+
55
+ public getPageLocatorString(property: string, componentName: SubjectComponents): string {
56
+ const elementId = this._namingHandler.getIdFromElementName(property);
57
+ const selectorPrefix = this.getDefaultPrefix(componentName);
58
+ const defaultSuffix = this.getDefaultSuffix(componentName);
59
+ const selectorSuffix = this.determineSelectorSuffix(property, componentName) || defaultSuffix;
60
+
61
+ return `page.${this.getLocatorType(componentName)}('${selectorPrefix}${elementId}${selectorSuffix}')`;
62
+ }
63
+
64
+ private getLocatorType(componentName: SubjectComponents): LocatorType {
65
+ if (componentName === 'Form') {
66
+ return 'getByTestId';
67
+ } else if (componentName === 'Grid') {
68
+ return 'locator';
69
+ } else {
70
+ throw new Error(`Unsupported component type: ${componentName}`);
71
+ }
72
+ }
73
+
74
+ public getPageContext(subject: ISubject) {
75
+ const variantSelector: string = subject.variant ? `[data-tab-variant-id="${subject.variant}"]` : '';
76
+ const screentypeId = subject.screentype_id.replace(/_/g, '-');
77
+
78
+ return `[data-testid="screen__${screentypeId}"][data-tab-id="${subject.subject}"]${variantSelector}`;
79
+ }
80
+ }
81
+
82
+ export type LocatorType = 'locator' | 'getByTestId' | 'getByRole' | 'getByLabel' | 'getByPlaceholder' | 'getByText';
@@ -0,0 +1,186 @@
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: reason 42 */
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { Form, Grid } from '../components/index.js';
7
+ import type { SubjectComponents } from '../types/Components.js';
8
+ import { NamingHandler } from './helpers/NamingHandler.js';
9
+ import { SelectorBuilder } from './SelectorBuilder.js';
10
+
11
+ export class SubjectComponentGenerator {
12
+ private static readonly SUBJECTS_RELATIVE_PATH = 'test-artifacts/subjects';
13
+
14
+ private readonly _currentFilename: string;
15
+ private readonly _currentDirname: string;
16
+ private readonly _subjectsDirectory: string;
17
+ private readonly _selectorBuilder: SelectorBuilder;
18
+ private readonly _namingHandler: NamingHandler;
19
+
20
+ constructor() {
21
+ this._currentFilename = fileURLToPath(import.meta.url);
22
+ this._currentDirname = dirname(this._currentFilename);
23
+ this._subjectsDirectory = path.resolve(
24
+ this._currentDirname,
25
+ `../../${SubjectComponentGenerator.SUBJECTS_RELATIVE_PATH}`
26
+ );
27
+ this._selectorBuilder = new SelectorBuilder();
28
+ this._namingHandler = new NamingHandler();
29
+ }
30
+
31
+ public run(): void {
32
+ this.refineSubjectComponents();
33
+ }
34
+
35
+ private getAllSubjectFolders(): string[] {
36
+ return fs.readdirSync(this._subjectsDirectory).filter((file) => {
37
+ return fs.statSync(path.join(this._subjectsDirectory, file)).isDirectory();
38
+ });
39
+ }
40
+
41
+ private refineSubjectComponents(): void {
42
+ const subjectFolders = this.getAllSubjectFolders();
43
+
44
+ subjectFolders.forEach((subjectFolder) => {
45
+ const componentsDirectory = path.join(this._subjectsDirectory, `${subjectFolder}/Components`);
46
+
47
+ if (!fs.existsSync(componentsDirectory) || !fs.statSync(componentsDirectory).isDirectory()) return;
48
+
49
+ const componentToRefine = new Map<string, typeof Grid | typeof Form>([
50
+ ['Grid', Grid],
51
+ ['Form', Form]
52
+ ]);
53
+
54
+ const componentFiles = fs.readdirSync(componentsDirectory);
55
+
56
+ componentFiles.forEach((componentFile) => {
57
+ const componentName = this.getComponentNameFromFile(componentFile);
58
+ const componentClass = componentToRefine.get(componentName);
59
+
60
+ if (componentClass) {
61
+ this.refine(path.join(componentsDirectory, componentFile), componentClass);
62
+ }
63
+ });
64
+ });
65
+ }
66
+
67
+ private getComponentNameFromFile(fileName: string): string {
68
+ const componentName = fileName
69
+ .split(/(?=[A-Z])/)
70
+ .pop()
71
+ ?.replace('.ts', '');
72
+
73
+ if (!componentName) throw new Error(`Could not extract component name from file: ${fileName}`);
74
+
75
+ return componentName;
76
+ }
77
+
78
+ private refine<T>(componentDirPath: string, component: T): void {
79
+ if (fs.existsSync(componentDirPath)) {
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ const componentName: SubjectComponents = (component as any).name ?? component;
82
+ const componentFile: { content: string } = { content: fs.readFileSync(componentDirPath, 'utf-8') };
83
+
84
+ this.removeLocatorInterface(componentFile);
85
+ this.addComponentImports(componentFile, componentName);
86
+ this.setSubjectExtendsComponent(componentFile, componentName);
87
+
88
+ const interfaceMatches: RegExpMatchArray | null = componentFile.content.match(/export interface (\w+) extends/);
89
+
90
+ if (interfaceMatches) {
91
+ const mainInterface = interfaceMatches[1];
92
+ const matchingProperties = this.getPropertyNamesFromInterface(componentFile, mainInterface);
93
+ let propertyDeclarations = '';
94
+ let assignments = '';
95
+
96
+ if (matchingProperties?.[1]) {
97
+ const propertyLines: string[] = this.convertStringsToArray(matchingProperties[1]);
98
+
99
+ propertyDeclarations = this.generatePropertyDeclarations(propertyLines);
100
+
101
+ assignments = this.getPropertyInitializations(propertyLines, componentName);
102
+ }
103
+
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`;
105
+ }
106
+
107
+ const subjectMatch: RegExpMatchArray | null = componentDirPath.match(/subjects[/](\w+)[/]/);
108
+ const subjectName: string | null = subjectMatch ? subjectMatch[1] : null;
109
+
110
+ if (subjectName) {
111
+ const formattedSubjectName = this._namingHandler.formatSubjectName(subjectName);
112
+ const subjectFolder = this.getSubjectFolderPath(formattedSubjectName);
113
+ const fileName = path.basename(componentDirPath);
114
+
115
+ this.verifyDirectoryExists(subjectFolder);
116
+
117
+ fs.writeFileSync(path.join(subjectFolder, fileName), componentFile.content, 'utf-8');
118
+ } else {
119
+ fs.writeFileSync(componentDirPath, componentFile.content, 'utf-8');
120
+ }
121
+ }
122
+ }
123
+
124
+ private getSubjectFolderPath(formattedSubjectName: string): string {
125
+ return path.resolve(
126
+ this._currentDirname,
127
+ `../../${SubjectComponentGenerator.SUBJECTS_RELATIVE_PATH}/${formattedSubjectName}/Components/`
128
+ );
129
+ }
130
+
131
+ private verifyDirectoryExists(directoryPath: string): void {
132
+ if (!fs.existsSync(directoryPath)) {
133
+ fs.mkdirSync(directoryPath, { recursive: true });
134
+ }
135
+ }
136
+
137
+ private getPropertyInitializations(propertyLines: string[], componentName: SubjectComponents): string {
138
+ return propertyLines
139
+ .map((line) => {
140
+ const property = line.split(':')[0].replace(/\?$/, '').trim();
141
+ const pageLocatorString = this._selectorBuilder.getPageLocatorString(property, componentName);
142
+ return ` this.${property} = ${pageLocatorString};`;
143
+ })
144
+ .join('\n');
145
+ }
146
+
147
+ private generatePropertyDeclarations(propertiesToConvert: string[]): string {
148
+ return propertiesToConvert
149
+ .map((line) => {
150
+ const property = line.replace(/^\s+/, '').split(':')[0].replace(/\?$/, '').trim();
151
+ return ` ${property}!: Locator;`;
152
+ })
153
+ .join('\n');
154
+ }
155
+
156
+ private convertStringsToArray(content: string): string[] {
157
+ return content
158
+ .split('\n')
159
+ .map((line) => line.trim())
160
+ .filter((line) => line && !line.startsWith('//'));
161
+ }
162
+
163
+ private getPropertyNamesFromInterface(
164
+ componentFile: { content: string },
165
+ mainInterface: string
166
+ ): RegExpMatchArray | null {
167
+ return componentFile.content.match(new RegExp(`export interface ${mainInterface} extends [^{]+{([^}]*)}`));
168
+ }
169
+
170
+ private setSubjectExtendsComponent(componentFile: { content: string }, componentName: string): void {
171
+ componentFile.content = componentFile.content.replace(
172
+ /export interface (\w+) \{/,
173
+ `export interface $1 extends ${componentName} {`
174
+ );
175
+ }
176
+
177
+ private addComponentImports(componentFile: { content: string }, componentName: string): void {
178
+ // biome-ignore lint/style/useTemplate: reason 42
179
+ componentFile.content = `import type { Locator, Page } from '@playwright/test';\n\n` + componentFile.content;
180
+ componentFile.content = `import { ${componentName} } from '../../../../components/index.js';\n${componentFile.content}`;
181
+ }
182
+
183
+ private removeLocatorInterface(componentFile: { content: string }): void {
184
+ componentFile.content = componentFile.content.replace(/export interface Locator {[^}]*}\s*/g, '');
185
+ }
186
+ }