@thinkwise/testwise 0.0.2-alpha.2
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/.ci/azure-pipelines.yaml +80 -0
- package/.eslintcache +1 -0
- package/.gitattributes +3 -0
- package/.gitconfig +2 -0
- package/.vscode/settings.json +18 -0
- package/Testwise.ts +22 -0
- package/components/BaseComponent.ts +95 -0
- package/components/BaseComponentObjects.ts +10 -0
- package/components/IComponentObjects.ts +5 -0
- package/components/Splitter.ts +11 -0
- package/components/TSFComponent.ts +8 -0
- package/components/actionbar/Actionbar.ts +57 -0
- package/components/actionbar/ActionbarObjects.ts +67 -0
- package/components/export/ExportComponent.ts +70 -0
- package/components/export/ExportComponentObjects.ts +23 -0
- package/components/filter/FilterForm.ts +11 -0
- package/components/filter/FindForm.ts +11 -0
- package/components/form/Form.ts +31 -0
- package/components/grid/Grid.ts +221 -0
- package/components/grid/GridObjects.ts +45 -0
- package/components/index.ts +6 -0
- package/components/pop-up/PopUpComponent.ts +14 -0
- package/components/pop-up/PopUpComponentModels.ts +13 -0
- package/components/tab/Tab.ts +29 -0
- package/components/task/TaskBar.ts +11 -0
- package/components/task/TaskTiles.ts +11 -0
- package/config.json +10 -0
- package/dist/Testwise.d.ts +2 -0
- package/dist/Testwise.js +14 -0
- package/dist/Testwise.js.map +1 -0
- package/dist/components/BaseComponent.d.ts +29 -0
- package/dist/components/BaseComponent.js +67 -0
- package/dist/components/BaseComponent.js.map +1 -0
- package/dist/components/BaseComponentObjects.d.ts +6 -0
- package/dist/components/BaseComponentObjects.js +6 -0
- package/dist/components/BaseComponentObjects.js.map +1 -0
- package/dist/components/IComponentObjects.d.ts +4 -0
- package/dist/components/IComponentObjects.js +2 -0
- package/dist/components/IComponentObjects.js.map +1 -0
- package/dist/components/Splitter.d.ts +5 -0
- package/dist/components/Splitter.js +10 -0
- package/dist/components/Splitter.js.map +1 -0
- package/dist/components/TSFComponent.d.ts +6 -0
- package/dist/components/TSFComponent.js +2 -0
- package/dist/components/TSFComponent.js.map +1 -0
- package/dist/components/actionbar/Actionbar.d.ts +27 -0
- package/dist/components/actionbar/Actionbar.js +35 -0
- package/dist/components/actionbar/Actionbar.js.map +1 -0
- package/dist/components/actionbar/ActionbarObjects.d.ts +24 -0
- package/dist/components/actionbar/ActionbarObjects.js +30 -0
- package/dist/components/actionbar/ActionbarObjects.js.map +1 -0
- package/dist/components/export/ExportComponent.d.ts +12 -0
- package/dist/components/export/ExportComponent.js +55 -0
- package/dist/components/export/ExportComponent.js.map +1 -0
- package/dist/components/export/ExportComponentObjects.d.ts +11 -0
- package/dist/components/export/ExportComponentObjects.js +12 -0
- package/dist/components/export/ExportComponentObjects.js.map +1 -0
- package/dist/components/form/Form.d.ts +7 -0
- package/dist/components/form/Form.js +31 -0
- package/dist/components/form/Form.js.map +1 -0
- package/dist/components/grid/Grid.d.ts +93 -0
- package/dist/components/grid/Grid.js +183 -0
- package/dist/components/grid/Grid.js.map +1 -0
- package/dist/components/grid/GridObjects.d.ts +20 -0
- package/dist/components/grid/GridObjects.js +25 -0
- package/dist/components/grid/GridObjects.js.map +1 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.js +7 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/pop-up/PopUpComponent.d.ts +7 -0
- package/dist/components/pop-up/PopUpComponent.js +9 -0
- package/dist/components/pop-up/PopUpComponent.js.map +1 -0
- package/dist/components/pop-up/PopUpComponentModels.d.ts +7 -0
- package/dist/components/pop-up/PopUpComponentModels.js +8 -0
- package/dist/components/pop-up/PopUpComponentModels.js.map +1 -0
- package/dist/config.json +10 -0
- package/dist/enums/ActionbarRegions.d.ts +5 -0
- package/dist/enums/ActionbarRegions.js +7 -0
- package/dist/enums/ActionbarRegions.js.map +1 -0
- package/dist/enums/ButtonEnums.d.ts +24 -0
- package/dist/enums/ButtonEnums.js +29 -0
- package/dist/enums/ButtonEnums.js.map +1 -0
- package/dist/enums/ExportFormat.d.ts +5 -0
- package/dist/enums/ExportFormat.js +7 -0
- package/dist/enums/ExportFormat.js.map +1 -0
- package/dist/enums/LogLevel.d.ts +8 -0
- package/dist/enums/LogLevel.js +11 -0
- package/dist/enums/LogLevel.js.map +1 -0
- package/dist/example-code/TestifyService.d.ts +22 -0
- package/dist/example-code/TestifyService.js +191 -0
- package/dist/example-code/TestifyService.js.map +1 -0
- package/dist/helpers/InflightRequestTracker.d.ts +10 -0
- package/dist/helpers/InflightRequestTracker.js +26 -0
- package/dist/helpers/InflightRequestTracker.js.map +1 -0
- package/dist/helpers/LoginHelper.d.ts +38 -0
- package/dist/helpers/LoginHelper.js +112 -0
- package/dist/helpers/LoginHelper.js.map +1 -0
- package/dist/helpers/UserSimulationHelper.d.ts +20 -0
- package/dist/helpers/UserSimulationHelper.js +62 -0
- package/dist/helpers/UserSimulationHelper.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/page-extensions/GoToDeepLink.d.ts +11 -0
- package/dist/page-extensions/GoToDeepLink.js +17 -0
- package/dist/page-extensions/GoToDeepLink.js.map +1 -0
- package/dist/page-extensions/LoginFeatures.d.ts +16 -0
- package/dist/page-extensions/LoginFeatures.js +30 -0
- package/dist/page-extensions/LoginFeatures.js.map +1 -0
- package/dist/page-extensions/UserSimulation.d.ts +15 -0
- package/dist/page-extensions/UserSimulation.js +30 -0
- package/dist/page-extensions/UserSimulation.js.map +1 -0
- package/dist/page-overrides/ClickOverride.d.ts +6 -0
- package/dist/page-overrides/ClickOverride.js +73 -0
- package/dist/page-overrides/ClickOverride.js.map +1 -0
- package/dist/services/ConfigBuilder.d.ts +12 -0
- package/dist/services/ConfigBuilder.js +30 -0
- package/dist/services/ConfigBuilder.js.map +1 -0
- package/dist/services/Logger.d.ts +8 -0
- package/dist/services/Logger.js +106 -0
- package/dist/services/Logger.js.map +1 -0
- package/dist/services/ReportingService.d.ts +8 -0
- package/dist/services/ReportingService.js +29 -0
- package/dist/services/ReportingService.js.map +1 -0
- package/dist/types/CoreTypes.d.ts +2 -0
- package/dist/types/CoreTypes.js +2 -0
- package/dist/types/CoreTypes.js.map +1 -0
- package/dist/types/universal.d.ts +25 -0
- package/dist/types/universal.js +2 -0
- package/dist/types/universal.js.map +1 -0
- package/enums/ActionbarRegions.ts +5 -0
- package/enums/ButtonEnums.ts +28 -0
- package/enums/ExportFormat.ts +5 -0
- package/enums/LogLevel.ts +9 -0
- package/example-code/TestifyService.ts +201 -0
- package/helpers/InflightRequestTracker.ts +33 -0
- package/helpers/LoginHelper.ts +140 -0
- package/helpers/UserSimulationHelper.ts +72 -0
- package/index.ts +28 -0
- package/package.json +40 -0
- package/page-extensions/GoToDeepLink.ts +29 -0
- package/page-extensions/LoginFeatures.ts +50 -0
- package/page-extensions/UserSimulation.ts +49 -0
- package/page-overrides/ClickOverride.ts +89 -0
- package/promptCredentials.js +113 -0
- package/scripts/Testwise.template.json +19 -0
- package/scripts/add-config.js +23 -0
- package/services/ConfigBuilder.ts +40 -0
- package/services/Logger.ts +125 -0
- package/services/ReportingService.ts +41 -0
- package/types/CoreTypes.ts +9 -0
- package/types/universal.ts +26 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/* Ignore this file for reviewing, it is used for Testify integration and only serves as an example*/
|
|
2
|
+
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
|
+
import axios, { type AxiosInstance } from 'axios';
|
|
5
|
+
|
|
6
|
+
export class TestifyService {
|
|
7
|
+
private token: string;
|
|
8
|
+
private projectId: string;
|
|
9
|
+
private baseUrl: string;
|
|
10
|
+
private axiosInstance: AxiosInstance;
|
|
11
|
+
testRunId = '';
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.token = `Bearer ${process.env.TESTIFY_TOKEN}`;
|
|
15
|
+
this.projectId = process.env.TESTIFY_PROJECT_ID || '';
|
|
16
|
+
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
|
17
|
+
this.baseUrl = process.env.TESTIFY_BASE_URL!.replace(/\/$/, '');
|
|
18
|
+
this.axiosInstance = axios.create({
|
|
19
|
+
baseURL: this.baseUrl,
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: this.token,
|
|
22
|
+
accept: '*/*'
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async createTestRun(): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
const projectName = await this.getProjectName();
|
|
30
|
+
const testRunName = `${projectName} ${new Date().toISOString()}`;
|
|
31
|
+
|
|
32
|
+
if (!this.testRunId) {
|
|
33
|
+
await this.createTestRunRequest(testRunName);
|
|
34
|
+
this.testRunId = await this.getTestRunId(testRunName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await new Promise((res) => setTimeout(res, 1000));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error('createTestRun failed:', err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async getTestRunId(testRunName: string): Promise<string> {
|
|
44
|
+
try {
|
|
45
|
+
const url = `/api/v1/TestRun?projectId=${this.projectId}`;
|
|
46
|
+
const response = await this.axiosInstance.get(url, { headers: this.headers('text/plain') });
|
|
47
|
+
const testRunList = response.data;
|
|
48
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
49
|
+
const found = testRunList.find((tr: any) => tr.name?.toLowerCase() === testRunName.toLowerCase());
|
|
50
|
+
return found?.id?.toString() ?? '';
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error('getTestRunId failed:', err);
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async createTestRunRequest(testRunName: string) {
|
|
58
|
+
try {
|
|
59
|
+
const encodedName = encodeURIComponent(testRunName);
|
|
60
|
+
const url = `/api/v1/TestRun/Create?testRunName=${encodedName}&projectId=${this.projectId}`;
|
|
61
|
+
await this.axiosInstance.post(url, null, { headers: this.headers() });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('createTestRunRequest failed:', err);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async getProjectName(): Promise<string> {
|
|
68
|
+
try {
|
|
69
|
+
const url = `/api/v1/Project/${this.projectId}`;
|
|
70
|
+
const response = await this.axiosInstance.get(url, { headers: this.headers('text/plain') });
|
|
71
|
+
const project = response.data;
|
|
72
|
+
return project.name;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error('getProjectName failed:', err);
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async reportTestResult(testName: string, passed: boolean) {
|
|
80
|
+
try {
|
|
81
|
+
const testCases = await this.getTestCases();
|
|
82
|
+
if (!this.testCaseExists(testCases, testName)) {
|
|
83
|
+
await this.createTestCase(testName);
|
|
84
|
+
}
|
|
85
|
+
const testCaseId = await this.getTestCaseId(testName);
|
|
86
|
+
if (!testCaseId) {
|
|
87
|
+
console.error(`Could not find or create test case ID for "${testName}"`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await this.addTestCaseToTestRun(testCaseId);
|
|
91
|
+
const testResultId = await this.getTestResultId(testCaseId);
|
|
92
|
+
if (!testResultId) {
|
|
93
|
+
console.error(`Could not find test result ID for test case "${testName}"`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await this.updateResult(testResultId, passed);
|
|
97
|
+
|
|
98
|
+
await new Promise((res) => setTimeout(res, 200));
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error('reportTestResult failed:', err);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async updateResult(testResultId: string, passed: boolean) {
|
|
105
|
+
try {
|
|
106
|
+
const status = passed ? '1' : '2';
|
|
107
|
+
const url = `/api/v1/Result/${testResultId}/UpdateStatus?newStatus=${status}`;
|
|
108
|
+
await this.axiosInstance.put(url, null, { headers: this.headers() });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error('updateResult failed:', err);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async getTestResultId(testCaseId: string): Promise<string> {
|
|
115
|
+
try {
|
|
116
|
+
const url = `/api/v1/Result?testRunId=${this.testRunId}`;
|
|
117
|
+
const response = await this.axiosInstance.get(url, { headers: this.headers('text/plain') });
|
|
118
|
+
const results = response.data;
|
|
119
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
120
|
+
const found = results.find((r: any) => r.testCaseId?.toString() === testCaseId);
|
|
121
|
+
return found?.id?.toString() ?? '';
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error('getTestResultId failed:', err);
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async getTestCaseId(testCaseName: string): Promise<string> {
|
|
129
|
+
try {
|
|
130
|
+
const testCases = await this.getTestCases();
|
|
131
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
132
|
+
const found = testCases.find((tc: any) => tc.name === testCaseName);
|
|
133
|
+
return found?.id?.toString() ?? '';
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error('getTestCaseId failed:', err);
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async addTestCaseToTestRun(testCaseId: string) {
|
|
141
|
+
try {
|
|
142
|
+
const url = `/api/v1/Result/Create?caseId=${testCaseId}&testRunId=${this.testRunId}`;
|
|
143
|
+
await this.axiosInstance.post(url, null, { headers: this.headers() });
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error('addTestCaseToTestRun failed:', err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async createTestCase(testCaseName: string) {
|
|
150
|
+
try {
|
|
151
|
+
const encodedName = encodeURIComponent(testCaseName);
|
|
152
|
+
const url = `/api/v1/TestCase/Create?testCaseName=${encodedName}&projectId=${this.projectId}`;
|
|
153
|
+
await this.axiosInstance.post(url, null, { headers: this.headers() });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error('createTestCase failed:', err);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
160
|
+
private testCaseExists(testCases: any[], testName: string): boolean {
|
|
161
|
+
try {
|
|
162
|
+
return testCases.some((tc) => tc.name?.toLowerCase() === testName.toLowerCase());
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error('testCaseExists failed:', err);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
170
|
+
private async getTestCases(): Promise<any[]> {
|
|
171
|
+
try {
|
|
172
|
+
const url = `/api/v1/TestCase?projectId=${this.projectId}`;
|
|
173
|
+
const response = await this.axiosInstance.get(url, { headers: this.headers('text/plain') });
|
|
174
|
+
return response.data;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error('getTestCases failed:', err);
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public async closeTestRun(): Promise<void> {
|
|
182
|
+
if (!this.testRunId) {
|
|
183
|
+
console.error('closeTestRun failed: testRunId is not set.');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const url = `/api/v1/TestRun/${this.testRunId}/Close`;
|
|
188
|
+
await this.axiosInstance.put(url, null, { headers: this.headers() });
|
|
189
|
+
console.info(`Test run ${this.testRunId} closed successfully.`);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error('closeTestRun failed:', err);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private headers(accept = '*/*') {
|
|
196
|
+
return {
|
|
197
|
+
accept,
|
|
198
|
+
Authorization: this.token
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Page, Request } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
export class InflightRequestsTracker {
|
|
4
|
+
private _page: Page;
|
|
5
|
+
private _requests: Set<Request>;
|
|
6
|
+
|
|
7
|
+
constructor(page: Page) {
|
|
8
|
+
this._page = page;
|
|
9
|
+
this._requests = new Set();
|
|
10
|
+
this._onStarted = this._onStarted.bind(this);
|
|
11
|
+
this._onFinished = this._onFinished.bind(this);
|
|
12
|
+
this._page.on('request', this._onStarted);
|
|
13
|
+
this._page.on('requestfinished', this._onFinished);
|
|
14
|
+
this._page.on('requestfailed', this._onFinished);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_onStarted(request: Request) {
|
|
18
|
+
this._requests.add(request);
|
|
19
|
+
}
|
|
20
|
+
_onFinished(request: Request) {
|
|
21
|
+
this._requests.delete(request);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
inflightRequests(): Request[] {
|
|
25
|
+
return Array.from(this._requests);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dispose() {
|
|
29
|
+
this._page.removeListener('request', this._onStarted);
|
|
30
|
+
this._page.removeListener('requestfinished', this._onFinished);
|
|
31
|
+
this._page.removeListener('requestfailed', this._onFinished);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { type Page, expect } from '@playwright/test';
|
|
5
|
+
import { testwiseConfig } from '../services/ConfigBuilder.js';
|
|
6
|
+
import type { UniversalConfigOptions, UniversalLoginOptions } from '../types/universal.js';
|
|
7
|
+
|
|
8
|
+
const _dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const configFixture = JSON.parse(fs.readFileSync(path.resolve(_dirname, '../config.json'), 'utf-8'));
|
|
10
|
+
|
|
11
|
+
export class LoginHelper {
|
|
12
|
+
private _page: Page;
|
|
13
|
+
private _serviceUrl: string;
|
|
14
|
+
private _metaEndpoint: string;
|
|
15
|
+
|
|
16
|
+
constructor(page: Page) {
|
|
17
|
+
this._page = page;
|
|
18
|
+
|
|
19
|
+
const serviceUrl = testwiseConfig().get<string>('environmentSettings.serviceUrl');
|
|
20
|
+
const metaEndpoint = testwiseConfig().get<string>('environmentSettings.metaEndpoint');
|
|
21
|
+
|
|
22
|
+
if (!serviceUrl || !metaEndpoint)
|
|
23
|
+
throw new Error(
|
|
24
|
+
'Either environmentSettings.serviceUrl or environmentSettings.metaEndpoint is not defined in the configuration.'
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
this._serviceUrl = serviceUrl;
|
|
28
|
+
this._metaEndpoint = metaEndpoint;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Log in and go to a specified application
|
|
33
|
+
* @param applicationName - Application name to launch
|
|
34
|
+
*/
|
|
35
|
+
public async logIn(applicationName: string = configFixture.defaultApplication) {
|
|
36
|
+
await this.logInAndConfigureApplication({
|
|
37
|
+
config: {
|
|
38
|
+
defaultApplication: applicationName
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Log in with credentials
|
|
45
|
+
* @param username - The username to log in with
|
|
46
|
+
* @param password - The password to log in with
|
|
47
|
+
*/
|
|
48
|
+
public async logInWithCredentials(username: string, password: string) {
|
|
49
|
+
const options: UniversalLoginOptions = {
|
|
50
|
+
username: username,
|
|
51
|
+
password: password
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
await this.logInWithOptions(options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Log in with options
|
|
59
|
+
* @param options - The login options
|
|
60
|
+
*/
|
|
61
|
+
public async logInWithOptions(options: UniversalLoginOptions) {
|
|
62
|
+
await this.logInAndConfigureApplication(options);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Navigate to the home page and wait for the menu to load
|
|
67
|
+
*/
|
|
68
|
+
private async navigateHomeAndWaitForMenuToLoad() {
|
|
69
|
+
await this._page.goto('/');
|
|
70
|
+
|
|
71
|
+
await this._page.route('**/i_ui_menu?**', (route) => {
|
|
72
|
+
route.continue();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await expect.poll(async () => await this._page.url()).toContain('#application=');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Log in and configure the application
|
|
80
|
+
* @param options - The login options
|
|
81
|
+
*/
|
|
82
|
+
private async logInAndConfigureApplication(options: UniversalLoginOptions = {}) {
|
|
83
|
+
const { serviceUrl, metaEndpoint, config } = options;
|
|
84
|
+
|
|
85
|
+
const username = options.username ?? process.env.SF_TEST_USERNAME;
|
|
86
|
+
const password = options.password ?? process.env.SF_TEST_PASSWORD;
|
|
87
|
+
|
|
88
|
+
if (!username || !password) {
|
|
89
|
+
throw new Error('Username and password are required for login.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (serviceUrl && metaEndpoint) {
|
|
93
|
+
this._serviceUrl = serviceUrl;
|
|
94
|
+
this._metaEndpoint = metaEndpoint;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const response = await this._page.request.post(`${this._serviceUrl}/account/api/login`, {
|
|
98
|
+
form: {
|
|
99
|
+
UserName: username,
|
|
100
|
+
Password: password
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (response.status() !== 204) {
|
|
105
|
+
console.error('Login failed', await response.status());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Set the navigator.language property
|
|
109
|
+
await this._page.route('**/*', (route, request) => {
|
|
110
|
+
route.continue({
|
|
111
|
+
headers: {
|
|
112
|
+
...request.headers(),
|
|
113
|
+
'Accept-Language': 'en-US'
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await this.configureApplication(config ?? {});
|
|
119
|
+
|
|
120
|
+
await this.navigateHomeAndWaitForMenuToLoad();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Configure the application
|
|
125
|
+
* @param options - The configuration options
|
|
126
|
+
*/
|
|
127
|
+
private async configureApplication(options: UniversalConfigOptions) {
|
|
128
|
+
await this._page.route('**/config.json', (route) => {
|
|
129
|
+
route.fulfill({
|
|
130
|
+
status: 200,
|
|
131
|
+
contentType: 'application/json',
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
...configFixture,
|
|
134
|
+
serviceUrl: `${this._serviceUrl}/iam/${this._metaEndpoint}`,
|
|
135
|
+
...options
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type Page, expect } from '@playwright/test';
|
|
2
|
+
import { testwiseConfig } from '../services/ConfigBuilder.js';
|
|
3
|
+
|
|
4
|
+
export class UserSimulationHelper {
|
|
5
|
+
private _page: Page;
|
|
6
|
+
private _serviceUrl: string;
|
|
7
|
+
|
|
8
|
+
constructor(page: Page) {
|
|
9
|
+
this._page = page;
|
|
10
|
+
|
|
11
|
+
const serviceUrl = testwiseConfig().get<string>('environmentSettings.serviceUrl');
|
|
12
|
+
|
|
13
|
+
if (!serviceUrl) throw new Error('environmentSettings.serviceUrl is not defined in the configuration.');
|
|
14
|
+
|
|
15
|
+
this._serviceUrl = serviceUrl;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the simulated user
|
|
20
|
+
* @returns The simulated user name
|
|
21
|
+
*/
|
|
22
|
+
public async getSimulatedUser(): Promise<string> {
|
|
23
|
+
const response = await this._page.request.get(`${this._serviceUrl}/account/api/usersimulation`);
|
|
24
|
+
|
|
25
|
+
switch (response.status()) {
|
|
26
|
+
case 204:
|
|
27
|
+
console.error('User simulation not active', await response.status());
|
|
28
|
+
return '';
|
|
29
|
+
case 200:
|
|
30
|
+
return (await response.json()).simulated_user;
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Get simulated user request failed with status: ${await response.status()}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stop the user simulation
|
|
38
|
+
*/
|
|
39
|
+
public async stopSimulation(): Promise<void> {
|
|
40
|
+
const response = await this._page.request.delete(`${this._serviceUrl}/account/api/usersimulation`);
|
|
41
|
+
|
|
42
|
+
if (response.status() === 204) {
|
|
43
|
+
console.info('STOP User simulation request is successful', await response.status());
|
|
44
|
+
} else {
|
|
45
|
+
throw new Error(`STOP User simulation has unexpected status: ${await response.status()}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await expect.poll(async () => await this.getSimulatedUser()).toBe('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Simulate a user
|
|
53
|
+
* @param userName - The name of the user to simulate
|
|
54
|
+
*/
|
|
55
|
+
public async simulateUser(userName: string): Promise<void> {
|
|
56
|
+
const response = await this._page.request.patch(`${this._serviceUrl}/account/api/usersimulation`, {
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
Accept: 'application/json'
|
|
60
|
+
},
|
|
61
|
+
data: JSON.stringify({
|
|
62
|
+
simulated_user: userName
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (response.status() === 200) {
|
|
67
|
+
console.info('User simulation request is successful', await response.status());
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error(`User simulation has unexpected status: ${await response.status()}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { logger } from './services/Logger.js';
|
|
2
|
+
|
|
3
|
+
const originalLog = console.log;
|
|
4
|
+
const originalInfo = console.info;
|
|
5
|
+
const originalWarn = console.warn;
|
|
6
|
+
const originalError = console.error;
|
|
7
|
+
const originalDebug = console.debug;
|
|
8
|
+
|
|
9
|
+
console.log = (...args: unknown[]) => logger.info(args.join(' '));
|
|
10
|
+
console.info = (...args: unknown[]) => logger.info(args.join(' '));
|
|
11
|
+
console.warn = (...args: unknown[]) => logger.warn(args.join(' '));
|
|
12
|
+
console.error = (...args: unknown[]) => logger.error(args.join(' '));
|
|
13
|
+
console.debug = (...args: unknown[]) => logger.debug(args.join(' '));
|
|
14
|
+
|
|
15
|
+
export { originalLog, originalInfo, originalWarn, originalError, originalDebug };
|
|
16
|
+
export * from './services/ConfigBuilder.js';
|
|
17
|
+
export * from './Testwise.js';
|
|
18
|
+
export * from './helpers/LoginHelper.js';
|
|
19
|
+
export * from './page-extensions/GoToDeepLink.js';
|
|
20
|
+
export * from './page-extensions/LoginFeatures.js';
|
|
21
|
+
export * from './page-extensions/UserSimulation.js';
|
|
22
|
+
export * from './page-overrides/ClickOverride.js';
|
|
23
|
+
export * from './types/CoreTypes.js';
|
|
24
|
+
export * from './types/universal.js';
|
|
25
|
+
export * from './enums/ButtonEnums.js';
|
|
26
|
+
export * from './enums/ActionbarRegions.js';
|
|
27
|
+
export * from './enums/ExportFormat.js';
|
|
28
|
+
export * from './components/index.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thinkwise/testwise",
|
|
3
|
+
"version": "0.0.2-alpha.2",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"testwise": "./scripts/add-config.js",
|
|
8
|
+
"testwise-credentials": "./promptCredentials.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"lint": "eslint ./ --cache --ignore-pattern .gitignore",
|
|
12
|
+
"format:fix": "run-s \"lint --fix\" && biome check --fix --unsafe",
|
|
13
|
+
"format:check": "run-s \"lint \" && biome check"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"description": "",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@types/node": "^22.15.29",
|
|
21
|
+
"axios": "^1.9.0",
|
|
22
|
+
"chalk": "^5.4.1",
|
|
23
|
+
"csv-parse": "^5.6.0",
|
|
24
|
+
"prompts": "^2.4.2"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@playwright/test": "^1.52.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@biomejs/biome": "^1.9.4",
|
|
31
|
+
"@eslint/js": "^9.28.0",
|
|
32
|
+
"@playwright/test": "^1.52.0",
|
|
33
|
+
"biome": "^0.3.3",
|
|
34
|
+
"eslint": "^9.28.0",
|
|
35
|
+
"eslint-plugin-playwright": "^2.2.0",
|
|
36
|
+
"npm-run-all": "^4.1.5",
|
|
37
|
+
"typescript-eslint": "^8.34.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": { "access": "public" }
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Test } from '../types/CoreTypes.js';
|
|
2
|
+
|
|
3
|
+
declare module '@playwright/test' {
|
|
4
|
+
interface Page {
|
|
5
|
+
goToDeepLink(deepLink: string): Promise<null | import('@playwright/test').Response>;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class GoToDeepLink {
|
|
10
|
+
private _test: Test;
|
|
11
|
+
|
|
12
|
+
constructor(test: Test) {
|
|
13
|
+
this._test = test.extend({
|
|
14
|
+
page: async ({ page }, use) => {
|
|
15
|
+
page.goToDeepLink = async (deepLink: string): Promise<null | import('@playwright/test').Response> => {
|
|
16
|
+
const url = new URL(await page.url());
|
|
17
|
+
|
|
18
|
+
return await page.goto(`${url}/${deepLink}`);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
await use(page);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get test() {
|
|
27
|
+
return this._test;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { LoginHelper } from '../helpers/LoginHelper.js';
|
|
2
|
+
import type { Test } from '../types/CoreTypes.js';
|
|
3
|
+
import type { UniversalLoginOptions } from '../types/universal.js';
|
|
4
|
+
|
|
5
|
+
declare module '@playwright/test' {
|
|
6
|
+
interface Page {
|
|
7
|
+
logIn(): Promise<void>;
|
|
8
|
+
logInWithCredentials(username: string, password: string): Promise<void>;
|
|
9
|
+
logInWithOptions(options: UniversalLoginOptions): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class LoginFeatures {
|
|
14
|
+
private _loginHelper: LoginHelper | undefined;
|
|
15
|
+
private _test: Test;
|
|
16
|
+
|
|
17
|
+
constructor(test: Test) {
|
|
18
|
+
this._test = test.extend({
|
|
19
|
+
page: async ({ page }, use) => {
|
|
20
|
+
this._loginHelper = new LoginHelper(page);
|
|
21
|
+
|
|
22
|
+
page.logIn = async () => {
|
|
23
|
+
await this.loginHelper.logIn();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
page.logInWithCredentials = async (username: string, password: string) => {
|
|
27
|
+
await this.loginHelper.logInWithCredentials(username, password);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
page.logInWithOptions = async (options: UniversalLoginOptions) => {
|
|
31
|
+
await this.loginHelper.logInWithOptions(options);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
await use(page);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private get loginHelper(): LoginHelper {
|
|
40
|
+
if (!this._loginHelper) {
|
|
41
|
+
throw new Error('LoginHelper is not initialized');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return this._loginHelper;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get test() {
|
|
48
|
+
return this._test;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { UserSimulationHelper } from '../helpers/UserSimulationHelper.js';
|
|
2
|
+
import type { Test } from '../types/CoreTypes.js';
|
|
3
|
+
|
|
4
|
+
declare module '@playwright/test' {
|
|
5
|
+
interface Page {
|
|
6
|
+
simulateUser(username: string): Promise<void>;
|
|
7
|
+
getSimulatedUser(): Promise<string>;
|
|
8
|
+
stopSimulation(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class UserSimulation {
|
|
13
|
+
private _userSimulationHelper: UserSimulationHelper | undefined;
|
|
14
|
+
private _test: Test;
|
|
15
|
+
|
|
16
|
+
constructor(test: Test) {
|
|
17
|
+
this._test = test.extend({
|
|
18
|
+
page: async ({ page }, use) => {
|
|
19
|
+
this._userSimulationHelper = new UserSimulationHelper(page);
|
|
20
|
+
|
|
21
|
+
page.simulateUser = async (username: string) => {
|
|
22
|
+
await this.userSimulationHelper.simulateUser(username);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
page.getSimulatedUser = async () => {
|
|
26
|
+
return await this.userSimulationHelper.getSimulatedUser();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
page.stopSimulation = async () => {
|
|
30
|
+
await this.userSimulationHelper.stopSimulation();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
await use(page);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private get userSimulationHelper(): UserSimulationHelper {
|
|
39
|
+
if (!this._userSimulationHelper) {
|
|
40
|
+
throw new Error('UserSimulationHelper is not initialized');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return this._userSimulationHelper;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get test() {
|
|
47
|
+
return this._test;
|
|
48
|
+
}
|
|
49
|
+
}
|