@thinkwise/testwise 0.2.0-beta.22 → 0.2.4

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 (115) hide show
  1. package/Testwise.ts +1 -3
  2. package/artifact-builder/InterfaceGenerator.ts +9 -1
  3. package/artifact-builder/ModelDataBuilder.ts +31 -60
  4. package/artifact-builder/ModelDataRefiner.ts +9 -5
  5. package/artifact-builder/SelectorBuilder.ts +8 -1
  6. package/artifact-builder/SubjectComponentGenerator.ts +77 -7
  7. package/artifact-builder/SubjectGenerator.ts +1 -1
  8. package/artifact-builder/SubjectRegistration.ts +68 -55
  9. package/artifact-builder/helpers/DataRetriever.ts +5 -2
  10. package/artifact-builder/helpers/NamingHandler.ts +25 -19
  11. package/components/BaseComponentObjects.ts +4 -1
  12. package/components/action-bar/ActionBar.ts +3 -3
  13. package/components/grid/Grid.ts +18 -21
  14. package/components/tab/BaseTab.ts +2 -2
  15. package/components/tab/BaseTabObjects.ts +1 -1
  16. package/components/tab/DetailTabPage.ts +2 -2
  17. package/components/tab/Tab.ts +4 -4
  18. package/controls/LookupDropdown.ts +7 -2
  19. package/dist/Testwise.js +1 -3
  20. package/dist/Testwise.js.map +1 -1
  21. package/dist/artifact-builder/InterfaceGenerator.d.ts +1 -0
  22. package/dist/artifact-builder/InterfaceGenerator.js +6 -1
  23. package/dist/artifact-builder/InterfaceGenerator.js.map +1 -1
  24. package/dist/artifact-builder/ModelDataBuilder.js +26 -50
  25. package/dist/artifact-builder/ModelDataBuilder.js.map +1 -1
  26. package/dist/artifact-builder/ModelDataRefiner.js +9 -5
  27. package/dist/artifact-builder/ModelDataRefiner.js.map +1 -1
  28. package/dist/artifact-builder/SelectorBuilder.js +3 -1
  29. package/dist/artifact-builder/SelectorBuilder.js.map +1 -1
  30. package/dist/artifact-builder/SubjectComponentGenerator.d.ts +4 -0
  31. package/dist/artifact-builder/SubjectComponentGenerator.js +61 -5
  32. package/dist/artifact-builder/SubjectComponentGenerator.js.map +1 -1
  33. package/dist/artifact-builder/SubjectGenerator.js +1 -1
  34. package/dist/artifact-builder/SubjectGenerator.js.map +1 -1
  35. package/dist/artifact-builder/SubjectRegistration.d.ts +9 -9
  36. package/dist/artifact-builder/SubjectRegistration.js +51 -42
  37. package/dist/artifact-builder/SubjectRegistration.js.map +1 -1
  38. package/dist/artifact-builder/helpers/DataRetriever.js +3 -2
  39. package/dist/artifact-builder/helpers/DataRetriever.js.map +1 -1
  40. package/dist/artifact-builder/helpers/NamingHandler.d.ts +2 -1
  41. package/dist/artifact-builder/helpers/NamingHandler.js +20 -15
  42. package/dist/artifact-builder/helpers/NamingHandler.js.map +1 -1
  43. package/dist/components/BaseComponentObjects.d.ts +1 -0
  44. package/dist/components/BaseComponentObjects.js +3 -1
  45. package/dist/components/BaseComponentObjects.js.map +1 -1
  46. package/dist/components/action-bar/ActionBar.d.ts +1 -1
  47. package/dist/components/action-bar/ActionBar.js +3 -3
  48. package/dist/components/grid/Grid.d.ts +5 -4
  49. package/dist/components/grid/Grid.js +13 -19
  50. package/dist/components/grid/Grid.js.map +1 -1
  51. package/dist/components/tab/BaseTab.d.ts +2 -2
  52. package/dist/components/tab/BaseTab.js +2 -2
  53. package/dist/components/tab/BaseTab.js.map +1 -1
  54. package/dist/components/tab/BaseTabObjects.js +1 -1
  55. package/dist/components/tab/BaseTabObjects.js.map +1 -1
  56. package/dist/components/tab/DetailTabPage.d.ts +2 -2
  57. package/dist/components/tab/DetailTabPage.js +2 -2
  58. package/dist/components/tab/DetailTabPage.js.map +1 -1
  59. package/dist/components/tab/Tab.d.ts +3 -3
  60. package/dist/components/tab/Tab.js +4 -4
  61. package/dist/components/tab/Tab.js.map +1 -1
  62. package/dist/controls/LookupDropdown.d.ts +3 -7
  63. package/dist/controls/LookupDropdown.js.map +1 -1
  64. package/dist/enums/ElementTypes.d.ts +1 -1
  65. package/dist/enums/ElementTypes.js +1 -1
  66. package/dist/enums/ElementTypes.js.map +1 -1
  67. package/dist/helpers/ConfigChecker.d.ts +3 -0
  68. package/dist/helpers/ConfigChecker.js +7 -0
  69. package/dist/helpers/ConfigChecker.js.map +1 -0
  70. package/dist/helpers/LoginHelper.js +1 -1
  71. package/dist/helpers/LoginHelper.js.map +1 -1
  72. package/dist/interfaces/IComponentObjects.d.ts +1 -0
  73. package/dist/page-extensions/SubjectRegistry.d.ts +0 -8
  74. package/dist/page-extensions/SubjectRegistry.js +2 -6
  75. package/dist/page-extensions/SubjectRegistry.js.map +1 -1
  76. package/dist/page-extensions/index.d.ts +0 -1
  77. package/dist/page-extensions/index.js +0 -1
  78. package/dist/page-extensions/index.js.map +1 -1
  79. package/dist/services/IndiciumApi.service.d.ts +27 -0
  80. package/dist/services/IndiciumApi.service.js +135 -0
  81. package/dist/services/IndiciumApi.service.js.map +1 -0
  82. package/dist/templates/test-artifacts/SubjectPageBase.d.ts +5 -0
  83. package/dist/templates/test-artifacts/SubjectPageBase.js +6 -0
  84. package/dist/templates/test-artifacts/SubjectPageBase.js.map +1 -0
  85. package/dist/templates/test-artifacts/screens/index.d.ts +1 -0
  86. package/dist/templates/test-artifacts/screens/index.js +2 -0
  87. package/dist/templates/test-artifacts/screens/index.js.map +1 -0
  88. package/dist/templates/test-artifacts/subjects/index.d.ts +1 -0
  89. package/dist/templates/test-artifacts/subjects/index.js +2 -0
  90. package/dist/templates/test-artifacts/subjects/index.js.map +1 -0
  91. package/enums/ElementTypes.ts +1 -1
  92. package/helpers/ConfigChecker.ts +7 -0
  93. package/helpers/LoginHelper.ts +1 -1
  94. package/interfaces/IComponentObjects.ts +1 -0
  95. package/package.json +3 -2
  96. package/page-extensions/SubjectRegistry.ts +2 -19
  97. package/page-extensions/index.ts +0 -1
  98. package/scripts/main.js +48 -69
  99. package/scripts/postinstall.js +40 -39
  100. package/scripts/sync.js +756 -102
  101. package/services/IndiciumApi.service.ts +159 -0
  102. package/templates/SubjectRegistry.template.ts +73 -0
  103. package/templates/test-artifacts/SubjectPageBase.ts +9 -0
  104. package/templates/test-artifacts/screens/index.ts +0 -0
  105. package/templates/test-artifacts/subjects/index.ts +0 -0
  106. package/tsconfig.json +2 -3
  107. package/dist/config.json +0 -10
  108. package/dist/page-extensions/SubjectProvider.d.ts +0 -11
  109. package/dist/page-extensions/SubjectProvider.js +0 -24
  110. package/dist/page-extensions/SubjectProvider.js.map +0 -1
  111. package/dist/test-artifacts/index.d.ts +0 -3
  112. package/dist/test-artifacts/index.js +0 -4
  113. package/dist/test-artifacts/index.js.map +0 -1
  114. package/page-extensions/SubjectProvider.ts +0 -41
  115. package/test-artifacts/index.ts +0 -3
package/Testwise.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { test as base } from '@playwright/test';
2
2
  import { test as bddBase } from 'playwright-bdd';
3
3
  import { Components, GoToDeepLink, LoginFeatures, UserSimulation } from './page-extensions/index.js';
4
- import { SubjectProvider } from './page-extensions/SubjectProvider.js';
5
4
  import { SubjectRegistry } from './page-extensions/SubjectRegistry.js';
6
5
  import { WaitEventHandler } from './page-extensions/WaitEventHandler.js';
7
6
  import { ClickOverride } from './page-overrides/ClickOverride.js';
@@ -23,8 +22,7 @@ const extensions = [
23
22
  LoginFeatures,
24
23
  UserSimulation,
25
24
  WaitEventHandler,
26
- SubjectRegistry,
27
- SubjectProvider
25
+ SubjectRegistry
28
26
  ];
29
27
 
30
28
  export const test: Test = combineExtensions(base, ...extensions);
@@ -4,17 +4,19 @@ import { fileURLToPath } from 'node:url';
4
4
  import * as prettier from 'prettier';
5
5
  import { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } from 'quicktype-core';
6
6
  import { PathResolver } from '../helpers/PathResolver.js';
7
- import { NamingHandler } from '../index.js';
7
+ import { DataRetriever, NamingHandler } from '../index.js';
8
8
 
9
9
  export class InterfaceGenerator {
10
10
  private readonly _namingHandler: NamingHandler;
11
11
  private _filename: string;
12
12
  private _dirname: string;
13
+ private _screensToBuild: string[];
13
14
 
14
15
  constructor() {
15
16
  this._namingHandler = new NamingHandler();
16
17
  this._filename = fileURLToPath(import.meta.url);
17
18
  this._dirname = path.dirname(this._filename);
19
+ this._screensToBuild = new DataRetriever().getScreensToBuild();
18
20
  }
19
21
 
20
22
  public async generateScreenInterfacesFromSchemas() {
@@ -137,6 +139,12 @@ export class InterfaceGenerator {
137
139
  }
138
140
 
139
141
  if (schemaType === SchemaType.Screen) {
142
+ const fileNameSnakeCase = this._namingHandler.pascalToSnakeCase(fileName);
143
+
144
+ if (this._screensToBuild.length > 0 && !this._screensToBuild.includes(fileNameSnakeCase)) {
145
+ return Promise.resolve(true);
146
+ }
147
+
140
148
  const perFileOutputPath = path.join(outputPath, `I${fileName}.ts`);
141
149
  return this.createInterfaceFromSchema(schemasDirectory, perFileOutputPath, file, true);
142
150
  }
@@ -3,11 +3,11 @@
3
3
  /** biome-ignore-all lint/suspicious/noExplicitAny: Because I can */
4
4
  import * as fs from 'node:fs/promises';
5
5
  import * as path from 'node:path';
6
- import axios from 'axios';
7
6
  import type { IProperty } from '../interfaces/IProperty.js';
8
- import { testwiseConfig } from '../services/ConfigBuilder.js';
7
+ import { indiciumApi } from '../services/IndiciumApi.service.js';
9
8
  import { DataRetriever } from './helpers/DataRetriever.js';
10
9
 
10
+ // ToDo: move this to a types folder
11
11
  type Subjects = {
12
12
  subject: string;
13
13
  variant: string;
@@ -16,30 +16,15 @@ type Subjects = {
16
16
  properties?: IProperty[];
17
17
  };
18
18
 
19
- const serviceUrl: string = testwiseConfig().get<string>('environmentSettings.serviceUrl')!.replace(/\/$/, '');
20
- const metaEndpoint: string = testwiseConfig()
21
- .get<string>('environmentSettings.metaEndpoint')!
22
- .replace(/^\//, '')
23
- .replace(/\/$/, '');
24
- const authUser: string = testwiseConfig().get<string>('environmentSettings.authUser')!;
25
- const authUserPassword: string = testwiseConfig().get<string>('environmentSettings.authUserPassword')!;
26
-
27
- const axiosInstance = axios.create({
28
- baseURL: serviceUrl,
29
- headers: {
30
- Authorization: `Basic ${Buffer.from(`${authUser}:${authUserPassword}`).toString('base64')}`,
31
- accept: '*/*'
32
- }
33
- });
34
-
35
- const guiApplId: number = await getGuiApplId();
36
-
37
19
  export async function buildSubjects() {
38
- const response = await axiosInstance.get(`/iam/${metaEndpoint}/i_ui_tab?$filter=gui_appl_id%20eq%20${guiApplId}`);
20
+ if (!(await indiciumApi.canConnectToProject())) {
21
+ console.log('Cannot connect to Indicium API. Aborting subject build.');
22
+ return;
23
+ }
39
24
 
40
- const rawTabs: any[] = response.data?.value || [];
25
+ const rawTables: any[] = await indiciumApi.getAllTables();
41
26
 
42
- const subjects: Subjects[] = rawTabs
27
+ const subjects: Subjects[] = rawTables
43
28
  .flatMap((tab: any) => {
44
29
  const base = {
45
30
  subject: tab.tab_id,
@@ -49,15 +34,23 @@ export async function buildSubjects() {
49
34
  return [
50
35
  { ...base, screentype_id: tab.main_screen_type_id, screentype_context: 'main' },
51
36
  { ...base, screentype_id: tab.detail_screen_type_id, screentype_context: 'detail' },
52
- { ...base, screentype_id: tab.zoom_screen_type_id, screentype_context: 'zoom' },
53
- { ...base, screentype_id: tab.popup_screen_type_id, screentype_context: 'popup' }
37
+ { ...base, screentype_id: tab.popup_screen_type_id, screentype_context: 'popup' },
38
+ { ...base, screentype_id: tab.zoom_screen_type_id, screentype_context: 'zoom' }
54
39
  ];
55
40
  })
56
41
  .filter((item) => item.screentype_id !== 'not_visible_in_gui');
57
42
 
58
- const colResponse = await axiosInstance.get(`/iam/${metaEndpoint}/i_ui_col?$filter=gui_appl_id%20eq%20${guiApplId}`);
43
+ const screentypeOrder = ['main', 'detail', 'popup', 'zoom'];
59
44
 
60
- const allProperties: IPropertyWithSubjectIdentifiers[] = colResponse.data?.value.map((col: any) => ({
45
+ subjects.sort((a, b) => {
46
+ return (
47
+ a.subject.localeCompare(b.subject) ||
48
+ a.variant.localeCompare(b.variant) ||
49
+ screentypeOrder.indexOf(a.screentype_context) - screentypeOrder.indexOf(b.screentype_context)
50
+ );
51
+ });
52
+
53
+ const allProperties: IPropertyWithSubjectIdentifiers[] = (await indiciumApi.getAllColumns()).map((col: any) => ({
61
54
  subject: col.tab_id,
62
55
  variant: col.tab_variant_id,
63
56
  col: col.col_id,
@@ -69,8 +62,10 @@ export async function buildSubjects() {
69
62
  (prop) => prop.subject === subject.subject && prop.variant === subject.variant
70
63
  );
71
64
 
72
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
73
- subject.properties = matchingProperties.map(({ subject, variant, ...rest }) => rest);
65
+ subject.properties = matchingProperties
66
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
67
+ .map(({ subject, variant, ...rest }) => rest)
68
+ .sort((a, b) => a.col.localeCompare(b.col));
74
69
  }
75
70
 
76
71
  const subjectsWithLookupsSet = await setLookupDropdowns(subjects);
@@ -83,11 +78,7 @@ export async function buildSubjects() {
83
78
  }
84
79
 
85
80
  async function setLookupDropdowns(subjects: Subjects[]): Promise<Subjects[]> {
86
- const response = await axiosInstance.get(
87
- `/iam/${metaEndpoint}/i_ui_tab_look_up?$filter=gui_appl_id%20eq%20${guiApplId}`
88
- );
89
-
90
- const lookupMappings: any[] = response.data?.value || [];
81
+ const lookupMappings: any[] = await indiciumApi.getLookups();
91
82
 
92
83
  for (const mapping of lookupMappings) {
93
84
  const targetSubject = subjects.find(
@@ -105,11 +96,12 @@ async function setLookupDropdowns(subjects: Subjects[]): Promise<Subjects[]> {
105
96
  }
106
97
 
107
98
  export async function buildScreens() {
108
- const response = await axiosInstance.get(
109
- `/iam/${metaEndpoint}/i_ui_screen_component?$filter=gui_appl_id eq ${guiApplId}`
110
- );
99
+ if (!(await indiciumApi.canConnectToProject())) {
100
+ console.log('Cannot connect to Indicium API. Aborting screen build.');
101
+ return;
102
+ }
111
103
 
112
- const rawComponents = response.data?.value || [];
104
+ const rawComponents = await indiciumApi.getScreenComponents();
113
105
  const screenMap = new Map<string, any>();
114
106
 
115
107
  for (const item of rawComponents) {
@@ -166,28 +158,7 @@ export async function buildScreens() {
166
158
  console.log('Screen schemas generated successfully.');
167
159
  }
168
160
 
169
- async function getGuiApplId(): Promise<number> {
170
- const guiApplAlias = testwiseConfig().get<string>('environmentSettings.guiApplAlias');
171
-
172
- if (guiApplAlias) {
173
- const response = await axiosInstance.get(`/iam/${metaEndpoint}/i_ui_gui_appl`);
174
- const guiAppl = response.data?.value.find((appl: any) => {
175
- const currentAlias = appl?.gui_appl_alias?.toString();
176
- const targetAlias = guiApplAlias?.toString();
177
-
178
- return currentAlias === targetAlias;
179
- });
180
-
181
- if (guiAppl) {
182
- return guiAppl.gui_appl_id;
183
- } else {
184
- throw new Error(`No GUI Application found for GUI Application Alias: ${guiApplAlias}`);
185
- }
186
- } else {
187
- throw new Error('GUI Application Alias is not defined in the configuration.');
188
- }
189
- }
190
-
161
+ // ToDo: move this to a interfaces folder
191
162
  interface IPropertyWithSubjectIdentifiers extends IProperty {
192
163
  subject: string;
193
164
  variant: string;
@@ -8,10 +8,16 @@ export class ModelDataRefiner {
8
8
 
9
9
  public run() {
10
10
  const seedDataDir = this._dataRetriever.getSeedDataDirectory();
11
+ const subjectsToBuildPath = path.resolve(seedDataDir, 'subjectsToBuild.json');
12
+ const screensToBuildPath = path.resolve(seedDataDir, 'screensToBuild.json');
11
13
  const registeredSubjects: IRegisteredSubjects[] | null = this._dataRetriever.getRegisteredSubjects();
12
14
 
13
15
  if (!registeredSubjects || registeredSubjects.length === 0) {
14
16
  console.info('No registered subjects found.');
17
+
18
+ if (fs.existsSync(screensToBuildPath)) fs.unlinkSync(screensToBuildPath);
19
+ if (fs.existsSync(subjectsToBuildPath)) fs.unlinkSync(subjectsToBuildPath);
20
+
15
21
  return;
16
22
  }
17
23
 
@@ -26,14 +32,12 @@ export class ModelDataRefiner {
26
32
  )
27
33
  );
28
34
 
29
- const subjectsToBuildPath = path.resolve(seedDataDir, 'subjectsToBuild.json');
30
35
  fs.writeFileSync(subjectsToBuildPath, JSON.stringify(subjectsToBuild, null, 2));
31
36
  console.log(`Created subjectsToBuild.json with ${subjectsToBuild.length} subjects.`);
32
37
 
33
- const screensToBuild = Array.from(new Set(subjectsToBuild.map((subject) => subject.screentype_id)));
38
+ const screensToBuildList = Array.from(new Set(subjectsToBuild.map((subject) => subject.screentype_id)));
34
39
 
35
- const screensToBuildPath = path.resolve(seedDataDir, 'screensToBuild.json');
36
- fs.writeFileSync(screensToBuildPath, JSON.stringify(screensToBuild, null, 2));
37
- console.log(`Created screensToBuild.json with ${screensToBuild.length} screens.`);
40
+ fs.writeFileSync(screensToBuildPath, JSON.stringify(screensToBuildList, null, 2));
41
+ console.log(`Created screensToBuild.json with ${screensToBuildList.length} screens.`);
38
42
  }
39
43
  }
@@ -53,7 +53,14 @@ export class SelectorBuilder {
53
53
  }
54
54
 
55
55
  public getPageLocatorString(property: string, componentName: SubjectComponents): string {
56
- const elementId = this._namingHandler.getIdFromElementName(property);
56
+ let elementId = this._namingHandler.getIdFromElementName(
57
+ property,
58
+ componentName,
59
+ this._namingHandler.getElementTypeFromElementName(property)
60
+ );
61
+
62
+ if (componentName === 'Grid') elementId = elementId.replace(/-/g, '_');
63
+
57
64
  const selectorPrefix = this.getDefaultPrefix(componentName);
58
65
  const defaultSuffix = this.getDefaultSuffix(componentName);
59
66
  const selectorSuffix = this.determineSelectorSuffix(property, componentName) || defaultSuffix;
@@ -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')
@@ -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,50 +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 _subjectBuilderPath: string;
14
13
 
15
14
  constructor() {
16
15
  this._currentFilename = fileURLToPath(import.meta.url);
17
16
  this._currentDirname = dirname(this._currentFilename);
18
17
  this._subjectTypePath = path.resolve(this._currentDirname, '../../page-extensions/SubjectRegistry.ts');
19
- this._subjectBuilderPath = path.resolve(this._currentDirname, '../../page-extensions/SubjectProvider.ts');
20
18
  }
21
19
 
22
20
  public run(): void {
23
- this.registerGeneratedSubjectsAsSubjectTypes();
24
- this.registerGeneratedSubjectsUnderPageGet();
21
+ const subjectDetails = this.getAllSubjectDetails();
22
+
23
+ this.registerGeneratedSubjectsAsSubjectTypes(subjectDetails);
25
24
  }
26
25
 
27
- private registerGeneratedSubjectsAsSubjectTypes(): void {
28
- const subjectList = this.getAllSubjects();
29
- const subjectTypeImportsAndExports = this.getSubjectTypesImportsAndExports(subjectList);
26
+ private async registerGeneratedSubjectsAsSubjectTypes(subjectDetails: SubjectDetail[]): Promise<void> {
27
+ const subjectTypeImportsAndExports = this.buildSubjectTypeExport(subjectDetails);
30
28
  const subject = { content: this.setSubjectTypeContent() };
31
29
 
32
30
  this.removeExistingSubjectTypeDefinition(subject);
33
31
  this.removeAllImportsFromSubjectIndex(subject);
34
32
 
35
33
  subject.content = subjectTypeImportsAndExports + subject.content;
36
- fs.writeFileSync(this._subjectTypePath, subject.content, 'utf-8');
37
- }
38
-
39
- private registerGeneratedSubjectsUnderPageGet(): void {
40
- const subjectNames = this.getAllSubjects();
41
- const subjectBuilder = { content: fs.readFileSync(this._subjectBuilderPath, 'utf-8') };
34
+ subject.content = this.addPathRecordsToContent(subject.content, subjectDetails);
42
35
 
43
- this.removeSubjectBuilderImports(subjectBuilder);
44
- this.removeSubjectBuilderSubjectDefinition(subjectBuilder);
45
- this.addSubjectBuilderImports(subjectNames, subjectBuilder);
46
- 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
+ });
47
42
 
48
- fs.writeFileSync(this._subjectBuilderPath, subjectBuilder.content, 'utf-8');
43
+ fs.writeFileSync(this._subjectTypePath, formattedContent, 'utf-8');
49
44
  }
50
45
 
51
46
  private removeExistingSubjectTypeDefinition(subject: { content: string }): void {
@@ -66,12 +61,15 @@ export class SubjectRegistration {
66
61
  }
67
62
  }
68
63
 
69
- private getSubjectTypesImportsAndExports(subjectList: string[]): string {
70
- const importLine = `import type { ${subjectList.join(', ')} } from '../${SubjectRegistration.SUBJECTS_RELATIVE_PATH}/index.js';`;
71
- const subjectTypeEntries = subjectList.map((name) => ` ${name}: typeof ${name};`).join('\n ');
72
- const newSubjectTypes = `${importLine}
73
-
74
- 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 = {
75
73
  ${subjectTypeEntries}
76
74
  };
77
75
  `;
@@ -79,53 +77,68 @@ export class SubjectRegistration {
79
77
  return newSubjectTypes;
80
78
  }
81
79
 
82
- 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[] {
83
97
  const subjectsDir = path.resolve(this._currentDirname, '../../test-artifacts/subjects');
84
98
 
85
99
  const subjectFolders = fs.readdirSync(subjectsDir).filter((file) => {
86
100
  return fs.statSync(path.join(subjectsDir, file)).isDirectory();
87
101
  });
88
102
 
89
- const subjectNames: string[] = [];
103
+ const subjectDetails: SubjectDetail[] = [];
104
+ const seenFullNames = new Set<string>();
90
105
 
91
106
  subjectFolders.forEach((folder) => {
92
107
  const folderPath = path.join(subjectsDir, folder);
93
- const tsFiles = fs.readdirSync(folderPath).filter((file) => file.endsWith('.ts'));
108
+ const tsFiles = fs.readdirSync(folderPath).filter((file) => file.endsWith('.ts') && file !== 'index.ts');
94
109
 
95
110
  tsFiles.forEach((tsFile) => {
96
111
  const classContent = fs.readFileSync(path.join(folderPath, tsFile), 'utf-8');
97
112
  const classMatches = classContent.matchAll(/export\s+class\s+(\w+)/g);
113
+
98
114
  for (const match of classMatches) {
99
- 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
+ }
100
133
  }
101
134
  });
102
135
  });
103
- return subjectNames;
104
- }
105
-
106
- private addSubjectBuilderSubjectDefinition(subjectNames: string[], subjectBuilder: { content: string }): void {
107
- const pageSubjectDefinition = `page.subject = {\n${subjectNames.map((name) => ` ${name},`).join('\n')}\n};\n`;
108
-
109
- subjectBuilder.content = subjectBuilder.content.replace(
110
- /(\/\/ page\.subject definition goes here\n)/,
111
- `$1 ${pageSubjectDefinition} `
112
- );
113
- }
114
-
115
- private removeSubjectBuilderSubjectDefinition(subjectBuilder: { content: string }): void {
116
- subjectBuilder.content = subjectBuilder.content.replace(/page\.subject\s*=\s*{[\s\S]*?};\s*/m, '');
117
- }
118
-
119
- private addSubjectBuilderImports(subjectNames: string[], subjectBuilder: { content: string }): void {
120
- const importLine = `import { ${subjectNames.join(', ')} } from '../${SubjectRegistration.SUBJECTS_RELATIVE_PATH}/index.js';`;
121
-
122
- subjectBuilder.content = subjectBuilder.content.replace(
123
- /(import type { Test } from '\.\.\/types\/Test\.js';)/,
124
- `$1\n${importLine}`
125
- );
136
+ return subjectDetails;
126
137
  }
138
+ }
127
139
 
128
- private removeSubjectBuilderImports(subjectBuilder: { content: string }): void {
129
- subjectBuilder.content = subjectBuilder.content.replace(SubjectRegistration.IMPORT_MATCH_REGEX, '');
130
- }
140
+ export interface SubjectDetail {
141
+ subject: string;
142
+ variant?: string;
143
+ screen: 'Main' | 'Popup' | 'Zoom' | 'Detail';
131
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
 
@@ -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.info(`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');