@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.
- package/Testwise.ts +1 -3
- package/artifact-builder/InterfaceGenerator.ts +9 -1
- package/artifact-builder/ModelDataBuilder.ts +31 -60
- package/artifact-builder/ModelDataRefiner.ts +9 -5
- package/artifact-builder/SelectorBuilder.ts +8 -1
- package/artifact-builder/SubjectComponentGenerator.ts +77 -7
- package/artifact-builder/SubjectGenerator.ts +1 -1
- package/artifact-builder/SubjectRegistration.ts +68 -55
- package/artifact-builder/helpers/DataRetriever.ts +5 -2
- package/artifact-builder/helpers/NamingHandler.ts +25 -19
- package/components/BaseComponentObjects.ts +4 -1
- package/components/action-bar/ActionBar.ts +3 -3
- package/components/grid/Grid.ts +18 -21
- package/components/tab/BaseTab.ts +2 -2
- package/components/tab/BaseTabObjects.ts +1 -1
- package/components/tab/DetailTabPage.ts +2 -2
- package/components/tab/Tab.ts +4 -4
- package/controls/LookupDropdown.ts +7 -2
- package/dist/Testwise.js +1 -3
- package/dist/Testwise.js.map +1 -1
- package/dist/artifact-builder/InterfaceGenerator.d.ts +1 -0
- package/dist/artifact-builder/InterfaceGenerator.js +6 -1
- package/dist/artifact-builder/InterfaceGenerator.js.map +1 -1
- package/dist/artifact-builder/ModelDataBuilder.js +26 -50
- package/dist/artifact-builder/ModelDataBuilder.js.map +1 -1
- package/dist/artifact-builder/ModelDataRefiner.js +9 -5
- package/dist/artifact-builder/ModelDataRefiner.js.map +1 -1
- package/dist/artifact-builder/SelectorBuilder.js +3 -1
- package/dist/artifact-builder/SelectorBuilder.js.map +1 -1
- package/dist/artifact-builder/SubjectComponentGenerator.d.ts +4 -0
- package/dist/artifact-builder/SubjectComponentGenerator.js +61 -5
- package/dist/artifact-builder/SubjectComponentGenerator.js.map +1 -1
- package/dist/artifact-builder/SubjectGenerator.js +1 -1
- package/dist/artifact-builder/SubjectGenerator.js.map +1 -1
- package/dist/artifact-builder/SubjectRegistration.d.ts +9 -9
- package/dist/artifact-builder/SubjectRegistration.js +51 -42
- package/dist/artifact-builder/SubjectRegistration.js.map +1 -1
- package/dist/artifact-builder/helpers/DataRetriever.js +3 -2
- package/dist/artifact-builder/helpers/DataRetriever.js.map +1 -1
- package/dist/artifact-builder/helpers/NamingHandler.d.ts +2 -1
- package/dist/artifact-builder/helpers/NamingHandler.js +20 -15
- package/dist/artifact-builder/helpers/NamingHandler.js.map +1 -1
- package/dist/components/BaseComponentObjects.d.ts +1 -0
- package/dist/components/BaseComponentObjects.js +3 -1
- package/dist/components/BaseComponentObjects.js.map +1 -1
- package/dist/components/action-bar/ActionBar.d.ts +1 -1
- package/dist/components/action-bar/ActionBar.js +3 -3
- package/dist/components/grid/Grid.d.ts +5 -4
- package/dist/components/grid/Grid.js +13 -19
- package/dist/components/grid/Grid.js.map +1 -1
- package/dist/components/tab/BaseTab.d.ts +2 -2
- package/dist/components/tab/BaseTab.js +2 -2
- package/dist/components/tab/BaseTab.js.map +1 -1
- package/dist/components/tab/BaseTabObjects.js +1 -1
- package/dist/components/tab/BaseTabObjects.js.map +1 -1
- package/dist/components/tab/DetailTabPage.d.ts +2 -2
- package/dist/components/tab/DetailTabPage.js +2 -2
- package/dist/components/tab/DetailTabPage.js.map +1 -1
- package/dist/components/tab/Tab.d.ts +3 -3
- package/dist/components/tab/Tab.js +4 -4
- package/dist/components/tab/Tab.js.map +1 -1
- package/dist/controls/LookupDropdown.d.ts +3 -7
- package/dist/controls/LookupDropdown.js.map +1 -1
- package/dist/enums/ElementTypes.d.ts +1 -1
- package/dist/enums/ElementTypes.js +1 -1
- package/dist/enums/ElementTypes.js.map +1 -1
- package/dist/helpers/ConfigChecker.d.ts +3 -0
- package/dist/helpers/ConfigChecker.js +7 -0
- package/dist/helpers/ConfigChecker.js.map +1 -0
- package/dist/helpers/LoginHelper.js +1 -1
- package/dist/helpers/LoginHelper.js.map +1 -1
- package/dist/interfaces/IComponentObjects.d.ts +1 -0
- package/dist/page-extensions/SubjectRegistry.d.ts +0 -8
- package/dist/page-extensions/SubjectRegistry.js +2 -6
- package/dist/page-extensions/SubjectRegistry.js.map +1 -1
- package/dist/page-extensions/index.d.ts +0 -1
- package/dist/page-extensions/index.js +0 -1
- package/dist/page-extensions/index.js.map +1 -1
- package/dist/services/IndiciumApi.service.d.ts +27 -0
- package/dist/services/IndiciumApi.service.js +135 -0
- package/dist/services/IndiciumApi.service.js.map +1 -0
- package/dist/templates/test-artifacts/SubjectPageBase.d.ts +5 -0
- package/dist/templates/test-artifacts/SubjectPageBase.js +6 -0
- package/dist/templates/test-artifacts/SubjectPageBase.js.map +1 -0
- package/dist/templates/test-artifacts/screens/index.d.ts +1 -0
- package/dist/templates/test-artifacts/screens/index.js +2 -0
- package/dist/templates/test-artifacts/screens/index.js.map +1 -0
- package/dist/templates/test-artifacts/subjects/index.d.ts +1 -0
- package/dist/templates/test-artifacts/subjects/index.js +2 -0
- package/dist/templates/test-artifacts/subjects/index.js.map +1 -0
- package/enums/ElementTypes.ts +1 -1
- package/helpers/ConfigChecker.ts +7 -0
- package/helpers/LoginHelper.ts +1 -1
- package/interfaces/IComponentObjects.ts +1 -0
- package/package.json +3 -2
- package/page-extensions/SubjectRegistry.ts +2 -19
- package/page-extensions/index.ts +0 -1
- package/scripts/main.js +48 -69
- package/scripts/postinstall.js +40 -39
- package/scripts/sync.js +756 -102
- package/services/IndiciumApi.service.ts +159 -0
- package/templates/SubjectRegistry.template.ts +73 -0
- package/templates/test-artifacts/SubjectPageBase.ts +9 -0
- package/templates/test-artifacts/screens/index.ts +0 -0
- package/templates/test-artifacts/subjects/index.ts +0 -0
- package/tsconfig.json +2 -3
- package/dist/config.json +0 -10
- package/dist/page-extensions/SubjectProvider.d.ts +0 -11
- package/dist/page-extensions/SubjectProvider.js +0 -24
- package/dist/page-extensions/SubjectProvider.js.map +0 -1
- package/dist/test-artifacts/index.d.ts +0 -3
- package/dist/test-artifacts/index.js +0 -4
- package/dist/test-artifacts/index.js.map +0 -1
- package/page-extensions/SubjectProvider.ts +0 -41
- 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 {
|
|
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
|
-
|
|
20
|
+
if (!(await indiciumApi.canConnectToProject())) {
|
|
21
|
+
console.log('Cannot connect to Indicium API. Aborting subject build.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
39
24
|
|
|
40
|
-
const
|
|
25
|
+
const rawTables: any[] = await indiciumApi.getAllTables();
|
|
41
26
|
|
|
42
|
-
const subjects: Subjects[] =
|
|
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.
|
|
53
|
-
{ ...base, screentype_id: tab.
|
|
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
|
|
43
|
+
const screentypeOrder = ['main', 'detail', 'popup', 'zoom'];
|
|
59
44
|
|
|
60
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
38
|
+
const screensToBuildList = Array.from(new Set(subjectsToBuild.map((subject) => subject.screentype_id)));
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 '../../
|
|
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.
|
|
24
|
-
|
|
21
|
+
const subjectDetails = this.getAllSubjectDetails();
|
|
22
|
+
|
|
23
|
+
this.registerGeneratedSubjectsAsSubjectTypes(subjectDetails);
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
private registerGeneratedSubjectsAsSubjectTypes(): void {
|
|
28
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
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
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
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.
|
|
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
|
|
48
|
+
console.info(`screensToBuild.json not found.`);
|
|
49
|
+
return [];
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
const json = fs.readFileSync(screensToBuildPath, 'utf-8');
|