@thinkwise/testwise 0.2.0-beta.3 → 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 +8 -7
- package/artifact-builder/ArtifactManager.ts +0 -1
- package/artifact-builder/InterfaceGenerator.ts +73 -83
- package/artifact-builder/ModelDataBuilder.ts +74 -86
- package/artifact-builder/ModelDataRefiner.ts +18 -13
- package/artifact-builder/SchemaGenerator.ts +1 -4
- package/artifact-builder/ScreenInterfaceRefiner.ts +0 -5
- package/artifact-builder/SelectorBuilder.ts +8 -1
- package/artifact-builder/SubjectComponentGenerator.ts +77 -7
- package/artifact-builder/SubjectGenerator.ts +3 -3
- package/artifact-builder/SubjectRegistration.ts +68 -60
- package/artifact-builder/helpers/DataRetriever.ts +6 -3
- package/artifact-builder/helpers/NamingHandler.ts +28 -20
- package/artifact-builder/helpers/Stopwatch.ts +13 -0
- package/artifact-builder/helpers/index.ts +1 -0
- 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.d.ts +1 -0
- package/dist/Testwise.js +15 -6
- package/dist/Testwise.js.map +1 -1
- package/dist/artifact-builder/ArtifactManager.js.map +1 -1
- package/dist/artifact-builder/InterfaceGenerator.d.ts +1 -1
- package/dist/artifact-builder/InterfaceGenerator.js +60 -67
- package/dist/artifact-builder/InterfaceGenerator.js.map +1 -1
- package/dist/artifact-builder/ModelDataBuilder.js +49 -60
- package/dist/artifact-builder/ModelDataBuilder.js.map +1 -1
- package/dist/artifact-builder/ModelDataRefiner.js +11 -7
- package/dist/artifact-builder/ModelDataRefiner.js.map +1 -1
- package/dist/artifact-builder/SchemaGenerator.js +0 -2
- package/dist/artifact-builder/SchemaGenerator.js.map +1 -1
- package/dist/artifact-builder/ScreenInterfaceRefiner.js +0 -5
- package/dist/artifact-builder/ScreenInterfaceRefiner.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 -10
- package/dist/artifact-builder/SubjectRegistration.js +51 -43
- package/dist/artifact-builder/SubjectRegistration.js.map +1 -1
- package/dist/artifact-builder/helpers/DataRetriever.js +4 -3
- 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 +23 -16
- package/dist/artifact-builder/helpers/NamingHandler.js.map +1 -1
- package/dist/artifact-builder/helpers/Stopwatch.d.ts +5 -0
- package/dist/artifact-builder/helpers/Stopwatch.js +13 -0
- package/dist/artifact-builder/helpers/Stopwatch.js.map +1 -0
- package/dist/artifact-builder/helpers/index.d.ts +1 -0
- package/dist/artifact-builder/helpers/index.js +1 -0
- package/dist/artifact-builder/helpers/index.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 +2 -2
- package/helpers/ConfigChecker.ts +7 -0
- package/helpers/LoginHelper.ts +1 -1
- package/interfaces/IComponentObjects.ts +1 -0
- package/interfaces/IRegisteredSubjects.ts +1 -1
- package/package.json +5 -3
- package/page-extensions/SubjectRegistry.ts +2 -19
- package/page-extensions/index.ts +0 -1
- package/scripts/main.js +63 -82
- package/scripts/postinstall.js +40 -42
- package/scripts/setup.js +37 -37
- package/scripts/sync.js +756 -69
- package/services/ConfigBuilder.ts +1 -1
- 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/types/Components.ts +1 -1
- 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
|
@@ -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')
|
|
@@ -57,7 +57,7 @@ export class SubjectGenerator {
|
|
|
57
57
|
const tsFiles = fs.readdirSync(subjectFolderPath).filter((file) => file.endsWith('.ts') && file !== 'index.ts');
|
|
58
58
|
const indexFile = path.join(subjectFolderPath, 'index.ts');
|
|
59
59
|
|
|
60
|
-
if(tsFiles.length === 0) return;
|
|
60
|
+
if (tsFiles.length === 0) return;
|
|
61
61
|
|
|
62
62
|
if (!fs.existsSync(indexFile)) {
|
|
63
63
|
fs.writeFileSync(indexFile, '', 'utf-8');
|
|
@@ -65,7 +65,7 @@ export class SubjectGenerator {
|
|
|
65
65
|
|
|
66
66
|
tsFiles.forEach((tsFile) => {
|
|
67
67
|
if (tsFile === 'index.ts') return;
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
const className = tsFile.replace('.ts', '');
|
|
70
70
|
exportLines.push(`export { ${className} } from './${className}.js';`);
|
|
71
71
|
});
|
|
@@ -82,7 +82,7 @@ export class SubjectGenerator {
|
|
|
82
82
|
const subjectFolderPath = this.getSubjectFolderRelativePath(subjectFolderName);
|
|
83
83
|
|
|
84
84
|
const subjectClass = {
|
|
85
|
-
content: `import type { Page } from '@playwright/test';\nimport { SubjectPageBase } from '../../
|
|
85
|
+
content: `import type { Page } from '@playwright/test';\nimport { SubjectPageBase } from '../../SubjectPageBase.js';\n`,
|
|
86
86
|
name: this._namingHandler.generateSubjectClassName(subject)
|
|
87
87
|
};
|
|
88
88
|
const componentDictionary: { name: string; type: SubjectAgnosticComponents; context: string }[] =
|
|
@@ -2,55 +2,45 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import * as prettier from 'prettier';
|
|
5
6
|
|
|
6
7
|
export class SubjectRegistration {
|
|
7
|
-
private static readonly SUBJECTS_RELATIVE_PATH = 'test-artifacts/subjects';
|
|
8
8
|
private static readonly IMPORT_MATCH_REGEX = /import .*'\.\.\/test-artifacts\/subjects\/index\.js';\n?/g;
|
|
9
9
|
|
|
10
10
|
private readonly _currentFilename: string;
|
|
11
11
|
private readonly _currentDirname: string;
|
|
12
12
|
private readonly _subjectTypePath: string;
|
|
13
|
-
private readonly _subjectsIndexDirectory: string;
|
|
14
|
-
private readonly _subjectBuilderPath: string;
|
|
15
13
|
|
|
16
14
|
constructor() {
|
|
17
15
|
this._currentFilename = fileURLToPath(import.meta.url);
|
|
18
16
|
this._currentDirname = dirname(this._currentFilename);
|
|
19
17
|
this._subjectTypePath = path.resolve(this._currentDirname, '../../page-extensions/SubjectRegistry.ts');
|
|
20
|
-
this._subjectsIndexDirectory = path.resolve(
|
|
21
|
-
this._currentDirname,
|
|
22
|
-
`../../${SubjectRegistration.SUBJECTS_RELATIVE_PATH}/index.ts`
|
|
23
|
-
);
|
|
24
|
-
this._subjectBuilderPath = path.resolve(this._currentDirname, '../../page-extensions/SubjectProvider.ts');
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
public run(): void {
|
|
28
|
-
this.
|
|
29
|
-
|
|
21
|
+
const subjectDetails = this.getAllSubjectDetails();
|
|
22
|
+
|
|
23
|
+
this.registerGeneratedSubjectsAsSubjectTypes(subjectDetails);
|
|
30
24
|
}
|
|
31
25
|
|
|
32
|
-
private registerGeneratedSubjectsAsSubjectTypes(): void {
|
|
33
|
-
const
|
|
34
|
-
const subjectTypeImportsAndExports = this.getSubjectTypesImportsAndExports(subjectList);
|
|
26
|
+
private async registerGeneratedSubjectsAsSubjectTypes(subjectDetails: SubjectDetail[]): Promise<void> {
|
|
27
|
+
const subjectTypeImportsAndExports = this.buildSubjectTypeExport(subjectDetails);
|
|
35
28
|
const subject = { content: this.setSubjectTypeContent() };
|
|
36
29
|
|
|
37
30
|
this.removeExistingSubjectTypeDefinition(subject);
|
|
38
31
|
this.removeAllImportsFromSubjectIndex(subject);
|
|
39
32
|
|
|
40
33
|
subject.content = subjectTypeImportsAndExports + subject.content;
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private registerGeneratedSubjectsUnderPageGet(): void {
|
|
45
|
-
const subjectNames = this.getAllSubjects();
|
|
46
|
-
const subjectBuilder = { content: fs.readFileSync(this._subjectBuilderPath, 'utf-8') };
|
|
34
|
+
subject.content = this.addPathRecordsToContent(subject.content, subjectDetails);
|
|
47
35
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
const formattedContent = await prettier.format(subject.content, {
|
|
37
|
+
parser: 'typescript',
|
|
38
|
+
singleQuote: true,
|
|
39
|
+
semi: true,
|
|
40
|
+
trailingComma: 'all'
|
|
41
|
+
});
|
|
52
42
|
|
|
53
|
-
fs.writeFileSync(this.
|
|
43
|
+
fs.writeFileSync(this._subjectTypePath, formattedContent, 'utf-8');
|
|
54
44
|
}
|
|
55
45
|
|
|
56
46
|
private removeExistingSubjectTypeDefinition(subject: { content: string }): void {
|
|
@@ -71,12 +61,15 @@ export class SubjectRegistration {
|
|
|
71
61
|
}
|
|
72
62
|
}
|
|
73
63
|
|
|
74
|
-
private
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
private buildSubjectTypeExport(subjectDetails: SubjectDetail[]): string {
|
|
65
|
+
const subjectTypeEntries = subjectDetails
|
|
66
|
+
.map(
|
|
67
|
+
(details) =>
|
|
68
|
+
` ${details.subject}${details.variant ?? ''}${details.screen}: ` +
|
|
69
|
+
`Promise<import('../test-artifacts/subjects/${details.subject}/index.js').${details.subject}${details.variant ?? ''}${details.screen}>;`
|
|
70
|
+
)
|
|
71
|
+
.join('\n ');
|
|
72
|
+
const newSubjectTypes = `export type SubjectType = {
|
|
80
73
|
${subjectTypeEntries}
|
|
81
74
|
};
|
|
82
75
|
`;
|
|
@@ -84,53 +77,68 @@ export class SubjectRegistration {
|
|
|
84
77
|
return newSubjectTypes;
|
|
85
78
|
}
|
|
86
79
|
|
|
87
|
-
private
|
|
80
|
+
private addPathRecordsToContent(content: string, subjectDetails: SubjectDetail[]): string {
|
|
81
|
+
const pathRecordsString = this.buildPathRecordsString(subjectDetails);
|
|
82
|
+
const stringToMatch = "none: ''";
|
|
83
|
+
return content.replace(stringToMatch, `\n${pathRecordsString}\n `);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private buildPathRecordsString(subjectDetails: SubjectDetail[]): string {
|
|
87
|
+
return subjectDetails
|
|
88
|
+
.map(
|
|
89
|
+
(details) =>
|
|
90
|
+
` ${details.subject}${details.variant ?? ''}${details.screen}: ` +
|
|
91
|
+
`'../test-artifacts/subjects/${details.subject}/index.js',`
|
|
92
|
+
)
|
|
93
|
+
.join('\n ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private getAllSubjectDetails(): SubjectDetail[] {
|
|
88
97
|
const subjectsDir = path.resolve(this._currentDirname, '../../test-artifacts/subjects');
|
|
89
98
|
|
|
90
99
|
const subjectFolders = fs.readdirSync(subjectsDir).filter((file) => {
|
|
91
100
|
return fs.statSync(path.join(subjectsDir, file)).isDirectory();
|
|
92
101
|
});
|
|
93
102
|
|
|
94
|
-
const
|
|
103
|
+
const subjectDetails: SubjectDetail[] = [];
|
|
104
|
+
const seenFullNames = new Set<string>();
|
|
95
105
|
|
|
96
106
|
subjectFolders.forEach((folder) => {
|
|
97
107
|
const folderPath = path.join(subjectsDir, folder);
|
|
98
|
-
const tsFiles = fs.readdirSync(folderPath).filter((file) => file.endsWith('.ts'));
|
|
108
|
+
const tsFiles = fs.readdirSync(folderPath).filter((file) => file.endsWith('.ts') && file !== 'index.ts');
|
|
99
109
|
|
|
100
110
|
tsFiles.forEach((tsFile) => {
|
|
101
111
|
const classContent = fs.readFileSync(path.join(folderPath, tsFile), 'utf-8');
|
|
102
112
|
const classMatches = classContent.matchAll(/export\s+class\s+(\w+)/g);
|
|
113
|
+
|
|
103
114
|
for (const match of classMatches) {
|
|
104
|
-
|
|
115
|
+
const className = match[1];
|
|
116
|
+
const pattern = new RegExp(`^(${folder})(.*)(Main|Popup|Zoom|Detail)$`);
|
|
117
|
+
const variantMatch = className.match(pattern);
|
|
118
|
+
|
|
119
|
+
if (variantMatch) {
|
|
120
|
+
const fullName = variantMatch[0];
|
|
121
|
+
if (!seenFullNames.has(fullName)) {
|
|
122
|
+
const variant = variantMatch[2] || '';
|
|
123
|
+
const screen = variantMatch[3] as SubjectDetail['screen'];
|
|
124
|
+
|
|
125
|
+
subjectDetails.push({
|
|
126
|
+
subject: folder,
|
|
127
|
+
variant: variant,
|
|
128
|
+
screen: screen
|
|
129
|
+
});
|
|
130
|
+
seenFullNames.add(fullName);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
105
133
|
}
|
|
106
134
|
});
|
|
107
135
|
});
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private addSubjectBuilderSubjectDefinition(subjectNames: string[], subjectBuilder: { content: string }): void {
|
|
112
|
-
const pageSubjectDefinition = `page.subject = {\n${subjectNames.map((name) => ` ${name},`).join('\n')}\n};\n`;
|
|
113
|
-
|
|
114
|
-
subjectBuilder.content = subjectBuilder.content.replace(
|
|
115
|
-
/(\/\/ page\.subject definition goes here\n)/,
|
|
116
|
-
`$1 ${pageSubjectDefinition} `
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private removeSubjectBuilderSubjectDefinition(subjectBuilder: { content: string }): void {
|
|
121
|
-
subjectBuilder.content = subjectBuilder.content.replace(/page\.subject\s*=\s*{[\s\S]*?};\s*/m, '');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private addSubjectBuilderImports(subjectNames: string[], subjectBuilder: { content: string }): void {
|
|
125
|
-
const importLine = `import { ${subjectNames.join(', ')} } from '../${SubjectRegistration.SUBJECTS_RELATIVE_PATH}/index.js';`;
|
|
126
|
-
|
|
127
|
-
subjectBuilder.content = subjectBuilder.content.replace(
|
|
128
|
-
/(import type { Test } from '\.\.\/types\/Test\.js';)/,
|
|
129
|
-
`$1\n${importLine}`
|
|
130
|
-
);
|
|
136
|
+
return subjectDetails;
|
|
131
137
|
}
|
|
138
|
+
}
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
export interface SubjectDetail {
|
|
141
|
+
subject: string;
|
|
142
|
+
variant?: string;
|
|
143
|
+
screen: 'Main' | 'Popup' | 'Zoom' | 'Detail';
|
|
136
144
|
}
|
|
@@ -19,8 +19,10 @@ export class DataRetriever {
|
|
|
19
19
|
|
|
20
20
|
public getRegisteredSubjects(): IRegisteredSubjects[] | null {
|
|
21
21
|
const registeredSubjectsPath = path.join(this._consumerRootDirectory, 'seed-data/registeredSubjects.json');
|
|
22
|
+
|
|
22
23
|
if (!fs.existsSync(registeredSubjectsPath)) {
|
|
23
|
-
console.
|
|
24
|
+
console.info(`Seed data file not found at: ${registeredSubjectsPath}`);
|
|
25
|
+
|
|
24
26
|
return null;
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -31,7 +33,7 @@ export class DataRetriever {
|
|
|
31
33
|
public getSubjectsToBuild(): ISubject[] {
|
|
32
34
|
const subjectsToBuildPath = path.join(this._consumerRootDirectory, 'seed-data/subjectsToBuild.json');
|
|
33
35
|
if (!fs.existsSync(subjectsToBuildPath)) {
|
|
34
|
-
console.
|
|
36
|
+
console.info(`subjectsToBuild.json not found at: ${subjectsToBuildPath}. Using all subjects as fallback.`);
|
|
35
37
|
|
|
36
38
|
return this.getSubjectsSeedData();
|
|
37
39
|
}
|
|
@@ -43,7 +45,8 @@ export class DataRetriever {
|
|
|
43
45
|
public getScreensToBuild(): string[] {
|
|
44
46
|
const screensToBuildPath = path.join(this._consumerRootDirectory, 'seed-data/screensToBuild.json');
|
|
45
47
|
if (!fs.existsSync(screensToBuildPath)) {
|
|
46
|
-
console.
|
|
48
|
+
console.info(`screensToBuild.json not found.`);
|
|
49
|
+
return [];
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
const json = fs.readFileSync(screensToBuildPath, 'utf-8');
|
|
@@ -25,7 +25,8 @@ export class NamingHandler {
|
|
|
25
25
|
Form: {
|
|
26
26
|
email: formDefaultSuffix,
|
|
27
27
|
phone_number: formDefaultSuffix,
|
|
28
|
-
suggestion_starts_with:
|
|
28
|
+
suggestion_starts_with: formDefaultSuffix,
|
|
29
|
+
lookup_dropdown: KnownElementTypes.Lookup,
|
|
29
30
|
checkbox: KnownElementTypes.Checkbox,
|
|
30
31
|
multiline: KnownElementTypes.Textarea,
|
|
31
32
|
combo: KnownElementTypes.Dropdown
|
|
@@ -34,6 +35,7 @@ export class NamingHandler {
|
|
|
34
35
|
email: gridDefaultSuffix,
|
|
35
36
|
phone_number: gridDefaultSuffix,
|
|
36
37
|
suggestion_starts_with: gridDefaultSuffix,
|
|
38
|
+
lookup_dropdown: gridDefaultSuffix,
|
|
37
39
|
checkbox: gridDefaultSuffix,
|
|
38
40
|
multiline: gridDefaultSuffix,
|
|
39
41
|
combo: gridDefaultSuffix
|
|
@@ -46,17 +48,16 @@ export class NamingHandler {
|
|
|
46
48
|
).toLowerCase();
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
public getIdFromElementName(elementName: string): string {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
);
|
|
51
|
+
public getIdFromElementName(elementName: string, componentType: SubjectComponents, controlType: string): string {
|
|
52
|
+
const elementSuffixToRemove = this.getElementSuffixFromControlTypeAndComponentType(controlType, componentType);
|
|
53
|
+
const snakeCaseElementName = this.pascalToSnakeCase(elementName);
|
|
54
|
+
|
|
55
|
+
return snakeCaseElementName
|
|
56
|
+
.replace(new RegExp(`${elementSuffixToRemove}$`, 'i'), '')
|
|
57
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/_/g, '-')
|
|
60
|
+
.replace(/-+$/g, '');
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
public getComponentTypeFromFileName(fileName: string): SubjectComponents {
|
|
@@ -71,18 +72,18 @@ export class NamingHandler {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
public getElementTypeFromElementName(elementName: string): string {
|
|
74
|
-
|
|
75
|
-
const
|
|
75
|
+
const snakeCaseElementName = this.pascalToSnakeCase(elementName);
|
|
76
|
+
const parts = snakeCaseElementName.split('_');
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (Object.values(KnownElementTypes).includes(
|
|
81
|
-
return
|
|
78
|
+
for (let i = parts.length - 1; i > 0; i--) {
|
|
79
|
+
const candidate = parts.slice(i).join('_');
|
|
80
|
+
|
|
81
|
+
if (Object.values(KnownElementTypes).includes(candidate as KnownElementTypes)) {
|
|
82
|
+
return candidate;
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
throw new Error(`
|
|
86
|
+
throw new Error(`No KnownElementType match found for a partial suffix of "${elementName}"`);
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
public getSubjectFromFileName(fileName: string): string {
|
|
@@ -136,6 +137,13 @@ export class NamingHandler {
|
|
|
136
137
|
.join('');
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
public pascalToSnakeCase(value: string): string {
|
|
141
|
+
return value
|
|
142
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
143
|
+
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
|
144
|
+
.toLowerCase();
|
|
145
|
+
}
|
|
146
|
+
|
|
139
147
|
public getComponentIdFromName(componentName: string, componentType: ScreenComponents): string {
|
|
140
148
|
if (componentType === 'ActionBar' || componentType === 'CustomActionBar') {
|
|
141
149
|
const capitalizedComponentName = this.capitalizeFirstLetter(componentName);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
|
|
3
|
+
export class Stopwatch {
|
|
4
|
+
start(message: string) {
|
|
5
|
+
const startTime = performance.now();
|
|
6
|
+
return {
|
|
7
|
+
stop() {
|
|
8
|
+
const duration = ((performance.now() - startTime) / 1000).toFixed(3);
|
|
9
|
+
console.log(`[${duration}s]: ${message}`);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -3,8 +3,11 @@ import type { IComponentObjects } from '../interfaces/IComponentObjects.js';
|
|
|
3
3
|
|
|
4
4
|
export abstract class BaseComponentObjects implements IComponentObjects {
|
|
5
5
|
context: Locator;
|
|
6
|
+
mainHeader: Locator;
|
|
6
7
|
|
|
7
8
|
constructor(page: Page, context: Locator | null = null) {
|
|
8
|
-
this.
|
|
9
|
+
this.mainHeader = page.locator('main header');
|
|
10
|
+
const body = page.locator('body');
|
|
11
|
+
this.context = context ? context : body;
|
|
9
12
|
}
|
|
10
13
|
}
|
|
@@ -90,9 +90,9 @@ export class ActionBar extends ActionBarActions {
|
|
|
90
90
|
private _actionBarObjects: ActionBarObjects;
|
|
91
91
|
private _overflowMenu: ActionBarOverflow;
|
|
92
92
|
|
|
93
|
-
constructor(page: Page,
|
|
94
|
-
super(page,
|
|
95
|
-
this._actionBarObjects = new ActionBarObjects(page,
|
|
93
|
+
constructor(page: Page, actionBarContext: Locator | null = null) {
|
|
94
|
+
super(page, actionBarContext);
|
|
95
|
+
this._actionBarObjects = new ActionBarObjects(page, actionBarContext);
|
|
96
96
|
this._overflowMenu = new ActionBarOverflow(page);
|
|
97
97
|
}
|
|
98
98
|
|
package/components/grid/Grid.ts
CHANGED
|
@@ -12,6 +12,10 @@ export class Grid {
|
|
|
12
12
|
this._page = page;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
protected get page() {
|
|
16
|
+
return this._page;
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
/**
|
|
16
20
|
* Retrieves a column header element by its text content.
|
|
17
21
|
* @param text
|
|
@@ -253,7 +257,7 @@ export class Grid {
|
|
|
253
257
|
const optionButton = this._gridObjects.excelStyleFilterPopupOptionByText(columnId, valueRegex);
|
|
254
258
|
await optionButton.locator('input[type="checkbox"]').check();
|
|
255
259
|
|
|
256
|
-
await this._gridObjects.
|
|
260
|
+
await this._gridObjects.mainHeader.click(); // Click outside to close the popup
|
|
257
261
|
await popup.waitFor({ state: 'detached' });
|
|
258
262
|
}
|
|
259
263
|
|
|
@@ -281,11 +285,11 @@ export class Grid {
|
|
|
281
285
|
}
|
|
282
286
|
|
|
283
287
|
/**
|
|
284
|
-
* Verifies the number of
|
|
285
|
-
* @param
|
|
288
|
+
* Verifies the number of records in the grid and checks for "No result" overlay if zero.
|
|
289
|
+
* @param expectedRecordCount The expected number of records.
|
|
286
290
|
*/
|
|
287
|
-
public async
|
|
288
|
-
if (
|
|
291
|
+
public async verifyGridRecordCount(expectedRecordCount: number): Promise<void> {
|
|
292
|
+
if (expectedRecordCount === 0) {
|
|
289
293
|
if (await this.hasNoRowsOverlay()) {
|
|
290
294
|
return;
|
|
291
295
|
} else {
|
|
@@ -294,29 +298,22 @@ export class Grid {
|
|
|
294
298
|
}
|
|
295
299
|
|
|
296
300
|
await pollUntil(() => this.rows().count(), {
|
|
297
|
-
predicate: (n: number) => n ===
|
|
301
|
+
predicate: (n: number) => n === expectedRecordCount + 1 // +1 to account for header row
|
|
298
302
|
});
|
|
299
303
|
}
|
|
300
304
|
|
|
301
305
|
/**
|
|
302
306
|
* Verifies if the "No result" overlay is visible in the grid.
|
|
303
|
-
* @returns A promise that resolves to true if the overlay is visible, otherwise false.
|
|
307
|
+
* @returns A promise that resolves to true if the overlay is visible and , otherwise false.
|
|
304
308
|
*/
|
|
305
309
|
public async hasNoRowsOverlay(): Promise<boolean> {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
},
|
|
313
|
-
{ predicate: Boolean }
|
|
314
|
-
);
|
|
315
|
-
} catch {
|
|
316
|
-
console.error('The "No result" overlay is not displaying the expected text.');
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
310
|
+
const overlay = this._gridObjects.noRowsOverlay();
|
|
311
|
+
|
|
312
|
+
const hasCorrectWording = await pollUntil(
|
|
313
|
+
async () => (await overlay.textContent())?.trim().includes('No result') ?? false,
|
|
314
|
+
{ predicate: Boolean }
|
|
315
|
+
);
|
|
319
316
|
|
|
320
|
-
return await
|
|
317
|
+
return hasCorrectWording && (await overlay.isVisible());
|
|
321
318
|
}
|
|
322
319
|
}
|
|
@@ -22,11 +22,11 @@ export class BaseTab {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// --- Useful getters ---
|
|
25
|
-
public
|
|
25
|
+
public getTabById(name: string): Locator {
|
|
26
26
|
return this._objects.tabByTestId(name);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
public
|
|
29
|
+
public getTabByIndex(index: number): Locator {
|
|
30
30
|
return this._objects.tabByIndex(index);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -12,7 +12,7 @@ export class BaseTabObjects extends BaseComponentObjects {
|
|
|
12
12
|
public tabList = (): Locator => this.context.getByRole('tablist');
|
|
13
13
|
|
|
14
14
|
public tabByTestId = (name: string): Locator =>
|
|
15
|
-
this.tabList().locator(`[role="tab"][data-testid^="tabstrip__tab__${name.toLowerCase()}
|
|
15
|
+
this.tabList().locator(`[role="tab"][data-testid^="tabstrip__tab__${name.toLowerCase()}"]`);
|
|
16
16
|
|
|
17
17
|
// use role for generic index-based selection
|
|
18
18
|
public tabByIndex = (index: number): Locator => this.tabList().getByRole('tab').nth(index);
|
|
@@ -10,11 +10,11 @@ export class DetailTabPage extends BaseTab {
|
|
|
10
10
|
this._objects = new DetailTabPageObjects(page, context);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
public
|
|
13
|
+
public getDetailTabPageById(name: string): Locator {
|
|
14
14
|
return this._objects.detailTab(name);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
public
|
|
17
|
+
public getDetailTabPageByIndex(index: number): Locator {
|
|
18
18
|
return this._objects.tabByIndex(index);
|
|
19
19
|
}
|
|
20
20
|
}
|
package/components/tab/Tab.ts
CHANGED
|
@@ -19,7 +19,7 @@ export class Tab extends BaseTab {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// Returns the “List” tab locator (scoped).
|
|
22
|
-
public
|
|
22
|
+
public getListTab(tabindex: string = '-1'): Locator {
|
|
23
23
|
return this._objects.listTab(tabindex);
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -29,12 +29,12 @@ export class Tab extends BaseTab {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// Returns the “Form” tab locator (scoped).
|
|
32
|
-
public
|
|
32
|
+
public getFormTab(): Locator {
|
|
33
33
|
return this._objects.formTab();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Opens a component tab by its ID in this tabstrip (scoped).
|
|
37
|
-
public async
|
|
38
|
-
await this._objects.tab(
|
|
37
|
+
public async openTabById(tabId: string): Promise<void> {
|
|
38
|
+
await this._objects.tab(tabId).click();
|
|
39
39
|
}
|
|
40
40
|
}
|