@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,89 @@
|
|
|
1
|
+
import { type Locator, expect } from '@playwright/test';
|
|
2
|
+
import { InflightRequestsTracker } from '../helpers/InflightRequestTracker.js';
|
|
3
|
+
import { testwiseConfig } from '../services/ConfigBuilder.js';
|
|
4
|
+
import type { Test } from '../types/CoreTypes.js';
|
|
5
|
+
|
|
6
|
+
export class ClickOverride {
|
|
7
|
+
private _test: Test;
|
|
8
|
+
|
|
9
|
+
constructor(test: Test) {
|
|
10
|
+
this._test = test.extend({
|
|
11
|
+
page: async ({ page }, use) => {
|
|
12
|
+
const overrideClickMethod = (locator: Locator) => {
|
|
13
|
+
const originalClickMethod = locator.click.bind(locator);
|
|
14
|
+
|
|
15
|
+
locator.click = async (options) => {
|
|
16
|
+
if (testwiseConfig().get<boolean>('InflightRequests.waitForRequestsToComplete') === true) {
|
|
17
|
+
const tracker = new InflightRequestsTracker(page);
|
|
18
|
+
|
|
19
|
+
await Promise.all([
|
|
20
|
+
originalClickMethod(options),
|
|
21
|
+
|
|
22
|
+
// Ensure at least one XHR request is issued
|
|
23
|
+
Promise.race([
|
|
24
|
+
page.waitForRequest((request) => request.resourceType() === 'xhr'),
|
|
25
|
+
new Promise((resolve) => setTimeout(resolve, 50)) // Wait briefly to ensure the XHR request is in flight
|
|
26
|
+
])
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
// Wait for all XHR requests to finish
|
|
30
|
+
try {
|
|
31
|
+
await expect
|
|
32
|
+
.poll(() => tracker.inflightRequests().filter((request) => request.resourceType() === 'xhr').length, {
|
|
33
|
+
timeout: 3000
|
|
34
|
+
})
|
|
35
|
+
.toBe(0);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.warn('Exception while waiting for inflight requests to finish:', e);
|
|
38
|
+
|
|
39
|
+
if (testwiseConfig().get<boolean>('InflightRequests.swallowPollingExceptions') === false) throw e;
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
await originalClickMethod(options);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return locator;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Override the getByTestId.click method
|
|
50
|
+
const originalGetByTestId = page.getByTestId.bind(page);
|
|
51
|
+
page.getByTestId = (...args) => overrideClickMethod(originalGetByTestId(...args));
|
|
52
|
+
|
|
53
|
+
// Override the locator.click method
|
|
54
|
+
const originalLocator = page.locator.bind(page);
|
|
55
|
+
page.locator = (...args) => overrideClickMethod(originalLocator(...args));
|
|
56
|
+
|
|
57
|
+
// Override the getByAltText.click method
|
|
58
|
+
const originalGetByAltText = page.getByAltText.bind(page);
|
|
59
|
+
page.getByAltText = (...args) => overrideClickMethod(originalGetByAltText(...args));
|
|
60
|
+
|
|
61
|
+
// Override the getByLabel.click method
|
|
62
|
+
const originalGetByLabel = page.getByLabel.bind(page);
|
|
63
|
+
page.getByLabel = (...args) => overrideClickMethod(originalGetByLabel(...args));
|
|
64
|
+
|
|
65
|
+
// Override the getByRole.click method
|
|
66
|
+
const originalGetByRole = page.getByRole.bind(page);
|
|
67
|
+
page.getByRole = (...args) => overrideClickMethod(originalGetByRole(...args));
|
|
68
|
+
|
|
69
|
+
// Override the getByPlaceholder.click method
|
|
70
|
+
const originalGetByPlaceholder = page.getByPlaceholder.bind(page);
|
|
71
|
+
page.getByPlaceholder = (...args) => overrideClickMethod(originalGetByPlaceholder(...args));
|
|
72
|
+
|
|
73
|
+
// Override the getByText.click method
|
|
74
|
+
const originalGetByText = page.getByText.bind(page);
|
|
75
|
+
page.getByText = (...args) => overrideClickMethod(originalGetByText(...args));
|
|
76
|
+
|
|
77
|
+
// Override the getByTitle.click method
|
|
78
|
+
const originalGetByTitle = page.getByTitle.bind(page);
|
|
79
|
+
page.getByTitle = (...args) => overrideClickMethod(originalGetByTitle(...args));
|
|
80
|
+
|
|
81
|
+
await use(page);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get test() {
|
|
87
|
+
return this._test;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import prompts from 'prompts';
|
|
7
|
+
|
|
8
|
+
const packageScripts = JSON.parse(readFileSync('./package.json')).scripts;
|
|
9
|
+
|
|
10
|
+
function exitWithError(error) {
|
|
11
|
+
console.log(chalk.red(error));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function startTestSolution() {
|
|
16
|
+
console.log('Now starting test solution...');
|
|
17
|
+
spawnSync(`yarn --cwd ../ ${args.join(' ')}`, { shell: true, stdio: 'inherit' });
|
|
18
|
+
process.exit();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
|
|
23
|
+
if (!args.length) {
|
|
24
|
+
exitWithError('Error: Got too few parameters. Usage: yarn test:credentials test');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const yarnScriptToExec = args[0];
|
|
28
|
+
|
|
29
|
+
if (!packageScripts[yarnScriptToExec]) {
|
|
30
|
+
exitWithError(`Error: Given script "${yarnScriptToExec}" doesn't exist in package.json.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// INCOMING HACK!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
34
|
+
// temp fix to be reworked when converting this file to TS
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
|
|
38
|
+
const indiciumURL = (() => {
|
|
39
|
+
try {
|
|
40
|
+
const configPath = path.resolve(process.cwd(), './Testwise.json');
|
|
41
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
42
|
+
return config.environmentSettings?.serviceUrl || '';
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error reading Testwise.json:', error);
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
// END OF HACK!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
50
|
+
|
|
51
|
+
if (process.env.SF_TEST_USERNAME && process.env.SF_TEST_PASSWORD) {
|
|
52
|
+
console.log('Using SF_TEST_USERNAME and SF_TEST_PASSWORD environment variables.');
|
|
53
|
+
startTestSolution();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const questions = [{
|
|
57
|
+
type: 'text',
|
|
58
|
+
name: 'username',
|
|
59
|
+
message: 'Username:',
|
|
60
|
+
validate: (value) => {
|
|
61
|
+
if (!value?.length) {
|
|
62
|
+
return 'Username is required';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (value.toLowerCase() === 'demo') {
|
|
66
|
+
return 'Running tests locally with demo is forbidden as it results in dataset concurrency problems'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}, {
|
|
72
|
+
type: 'invisible',
|
|
73
|
+
name: 'password',
|
|
74
|
+
message: 'Password:',
|
|
75
|
+
validate: (value) => !value?.length ? 'Password is required' : true
|
|
76
|
+
}];
|
|
77
|
+
|
|
78
|
+
(async () => {
|
|
79
|
+
const credentialsResponse = await prompts(questions);
|
|
80
|
+
|
|
81
|
+
if (!credentialsResponse || !credentialsResponse.username || !credentialsResponse.password) {
|
|
82
|
+
exitWithError('Username or password was not given');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { password, username } = credentialsResponse;
|
|
86
|
+
const params = new URLSearchParams();
|
|
87
|
+
|
|
88
|
+
params.append('UserName', username);
|
|
89
|
+
params.append('Password', password);
|
|
90
|
+
|
|
91
|
+
const fetch = (await import('node-fetch')).default;
|
|
92
|
+
const authResponse = await fetch(`${indiciumURL}/account/api/login`, { method: 'POST', body: params });
|
|
93
|
+
|
|
94
|
+
if (authResponse.status === 204) {
|
|
95
|
+
console.log('Confirmed credentials work @', indiciumURL);
|
|
96
|
+
} else {
|
|
97
|
+
const confirmResponse = await prompts([{
|
|
98
|
+
type: 'confirm',
|
|
99
|
+
name: 'value',
|
|
100
|
+
message: `Can't login to ${indiciumURL} with the given credentials. Continue anyways?`,
|
|
101
|
+
initial: true
|
|
102
|
+
}]);
|
|
103
|
+
|
|
104
|
+
if (!confirmResponse.value) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
process.env.SF_TEST_USERNAME = username;
|
|
110
|
+
process.env.SF_TEST_PASSWORD = password;
|
|
111
|
+
|
|
112
|
+
startTestSolution();
|
|
113
|
+
})();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"environmentSettings" :{
|
|
3
|
+
"baseUrl" : "https://develop.example.app",
|
|
4
|
+
"serviceUrl" : "https://develop.example.app/service",
|
|
5
|
+
"metaEndpoint" : "iam"
|
|
6
|
+
},
|
|
7
|
+
"featureSettings" :{
|
|
8
|
+
"logger" : {
|
|
9
|
+
"logDir" : "logs",
|
|
10
|
+
"logLevel" : "info",
|
|
11
|
+
"logByDay" : true,
|
|
12
|
+
"bufferSizeLimit" : -1
|
|
13
|
+
},
|
|
14
|
+
"InflightRequests" : {
|
|
15
|
+
"waitForRequestsToComplete" : true,
|
|
16
|
+
"swallowPollingExceptions" : true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-undef */
|
|
3
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
if (process.argv[2] === 'install') {
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const configFile = path.resolve(process.cwd(), 'Testwise.json');
|
|
13
|
+
const templateFile = path.join(__dirname, 'Testwise.template.json');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(configFile)) {
|
|
16
|
+
fs.copyFileSync(templateFile, configFile);
|
|
17
|
+
console.log('Testwise.json config file created.');
|
|
18
|
+
} else {
|
|
19
|
+
console.log('Testwise.json already exists, skipping creation.');
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
console.log('Unknown or missing command. Usage: npx testwise install');
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
class ConfigBuilder {
|
|
5
|
+
private config: Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
const configFilePath = path.resolve(process.cwd(), 'Testwise.json');
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(configFilePath)) {
|
|
11
|
+
throw new Error(`Config file not found at path: ${configFilePath}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const fileContent = fs.readFileSync(configFilePath, 'utf-8');
|
|
15
|
+
this.config = JSON.parse(fileContent);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves a value from the config using a dot-separated key.
|
|
20
|
+
* @param key - The dot-separated key to retrieve the value.
|
|
21
|
+
* @returns The value associated with the key, or undefined if not found.
|
|
22
|
+
*/
|
|
23
|
+
public get<T>(key: string): T | undefined {
|
|
24
|
+
return key
|
|
25
|
+
.split('.')
|
|
26
|
+
.reduce(
|
|
27
|
+
(obj: Record<string, unknown> | undefined, segment) => obj?.[segment] as Record<string, unknown>,
|
|
28
|
+
this.config
|
|
29
|
+
) as T;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let instance: ConfigBuilder | null = null;
|
|
34
|
+
|
|
35
|
+
export function testwiseConfig(): ConfigBuilder {
|
|
36
|
+
if (!instance) {
|
|
37
|
+
instance = new ConfigBuilder();
|
|
38
|
+
}
|
|
39
|
+
return instance;
|
|
40
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { LogLevel } from '../enums/LogLevel.js';
|
|
4
|
+
import { originalDebug, originalError, originalInfo, originalLog, originalWarn } from '../index.js';
|
|
5
|
+
import { testwiseConfig } from './ConfigBuilder.js';
|
|
6
|
+
|
|
7
|
+
const logDir = path.resolve(process.cwd(), testwiseConfig().get<string>('featureSettings.logger.logDir') ?? 'logs');
|
|
8
|
+
|
|
9
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
10
|
+
|
|
11
|
+
const sanitizedTimestamp = new Date().toISOString().split('T')[0];
|
|
12
|
+
const logFilePath = path.join(
|
|
13
|
+
logDir,
|
|
14
|
+
testwiseConfig().get<boolean>('featureSettings.logger.logByDay') === true
|
|
15
|
+
? `test-log-${sanitizedTimestamp}.log`
|
|
16
|
+
: 'test-log.log'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
let logBuffer: string[] = [];
|
|
20
|
+
const bufferSizeLimit = testwiseConfig().get<number>('featureSettings.logger.bufferSizeLimit') ?? 1000;
|
|
21
|
+
|
|
22
|
+
let linesAlreadyWritten = 0;
|
|
23
|
+
|
|
24
|
+
if (fs.existsSync(logFilePath)) {
|
|
25
|
+
const existingLogs = fs.readFileSync(logFilePath, 'utf-8').split('\n').filter(Boolean);
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
27
|
+
linesAlreadyWritten = existingLogs.length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Flushes the log buffer to the log file.
|
|
32
|
+
* @returns {void}
|
|
33
|
+
* @throws {Error} If the log buffer cannot be flushed.
|
|
34
|
+
*/
|
|
35
|
+
function flushLogs() {
|
|
36
|
+
try {
|
|
37
|
+
if (logBuffer.length > 0) {
|
|
38
|
+
const existingLogs = fs.existsSync(logFilePath)
|
|
39
|
+
? fs.readFileSync(logFilePath, 'utf-8').split('\n').filter(Boolean)
|
|
40
|
+
: [];
|
|
41
|
+
|
|
42
|
+
const combinedLogs = [...existingLogs, ...logBuffer];
|
|
43
|
+
const trimmedLogs = bufferSizeLimit > -1 ? combinedLogs.slice(-bufferSizeLimit) : combinedLogs;
|
|
44
|
+
|
|
45
|
+
fs.writeFileSync(logFilePath, `${trimmedLogs.join('\n')}\n`);
|
|
46
|
+
|
|
47
|
+
logBuffer = [];
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error('Failed to flush:', err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Logs a message to the console and a log file.
|
|
56
|
+
* @param message
|
|
57
|
+
* @param level
|
|
58
|
+
* @param optionalParams
|
|
59
|
+
*/
|
|
60
|
+
function log(message?: string, level: LogLevel = LogLevel.INFO, ...optionalParams: unknown[]): void {
|
|
61
|
+
const timestamp = new Date().toISOString();
|
|
62
|
+
const origin = getCallerInfo();
|
|
63
|
+
const formatted = `[${timestamp}] [${level}] [${origin}] ${message}`;
|
|
64
|
+
|
|
65
|
+
switch (level) {
|
|
66
|
+
case LogLevel.FATAL:
|
|
67
|
+
case LogLevel.ERROR:
|
|
68
|
+
originalError(formatted, ...optionalParams);
|
|
69
|
+
break;
|
|
70
|
+
case LogLevel.WARN:
|
|
71
|
+
originalWarn(formatted, ...optionalParams);
|
|
72
|
+
break;
|
|
73
|
+
case LogLevel.DEBUG:
|
|
74
|
+
originalDebug(formatted, ...optionalParams);
|
|
75
|
+
break;
|
|
76
|
+
case LogLevel.INFO:
|
|
77
|
+
originalInfo(formatted, ...optionalParams);
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
originalLog(formatted, ...optionalParams);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
logBuffer.push(`${formatted} ${optionalParams}`);
|
|
85
|
+
flushLogs();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Retrieves the caller information from the stack trace.
|
|
90
|
+
* @returns {string} The caller information in the format "file:line:column"
|
|
91
|
+
*/
|
|
92
|
+
function getCallerInfo(): string {
|
|
93
|
+
const stack = new Error().stack;
|
|
94
|
+
if (!stack) return 'unknown origin';
|
|
95
|
+
|
|
96
|
+
const stackLines = stack.split('\n');
|
|
97
|
+
|
|
98
|
+
for (const line of stackLines) {
|
|
99
|
+
if (!line.includes('Logger.ts') && !line.includes('index.ts')) {
|
|
100
|
+
const match = line.match(/\((.*):(\d+):(\d+)\)/);
|
|
101
|
+
if (match) {
|
|
102
|
+
let file = match[1];
|
|
103
|
+
const lineNumber = match[2];
|
|
104
|
+
const columnNumber = match[3];
|
|
105
|
+
|
|
106
|
+
if (file.startsWith('file:///')) {
|
|
107
|
+
file = decodeURI(file.replace('file:///', ''));
|
|
108
|
+
}
|
|
109
|
+
return `${file}:${lineNumber}:${columnNumber}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return 'unknown origin';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const logger = {
|
|
118
|
+
// Order by severity level
|
|
119
|
+
fatal: (msg?: string, ...optionalParams: unknown[]): void => log(msg, LogLevel.FATAL, ...optionalParams),
|
|
120
|
+
error: (msg?: string, ...optionalParams: unknown[]): void => log(msg, LogLevel.ERROR, ...optionalParams),
|
|
121
|
+
warn: (msg?: string, ...optionalParams: unknown[]): void => log(msg, LogLevel.WARN, ...optionalParams),
|
|
122
|
+
step: (msg?: string, ...optionalParams: unknown[]): void => log(msg, LogLevel.STEP, ...optionalParams),
|
|
123
|
+
info: (msg?: string, ...optionalParams: unknown[]): void => log(msg, LogLevel.INFO, ...optionalParams),
|
|
124
|
+
debug: (msg?: string, ...optionalParams: unknown[]): void => log(msg, LogLevel.DEBUG, ...optionalParams)
|
|
125
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// This is tightly coupled to the Testify reporting service integration and is subject to change.
|
|
2
|
+
|
|
3
|
+
import type { FullResult, Reporter, TestCase, TestResult } from '@playwright/test/reporter';
|
|
4
|
+
import { TestifyService } from '../example-code/TestifyService.js';
|
|
5
|
+
|
|
6
|
+
interface StoredTestResult {
|
|
7
|
+
title: string;
|
|
8
|
+
status: TestResult['status'];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class ReportingService implements Reporter {
|
|
12
|
+
private _testifyService: TestifyService | null = null;
|
|
13
|
+
private _results: StoredTestResult[] = [];
|
|
14
|
+
|
|
15
|
+
onTestEnd(test: TestCase, result: TestResult) {
|
|
16
|
+
console.info(`Test ended: ${test.title}, status: ${result.status}`);
|
|
17
|
+
|
|
18
|
+
if (process.env.REPORTING_ENABLED !== 'true') return;
|
|
19
|
+
|
|
20
|
+
this._results.push({ title: test.title, status: result.status });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async onEnd(result: FullResult) {
|
|
24
|
+
if (process.env.REPORTING_ENABLED !== 'true') return;
|
|
25
|
+
|
|
26
|
+
this._testifyService = new TestifyService();
|
|
27
|
+
|
|
28
|
+
await this._testifyService.createTestRun();
|
|
29
|
+
|
|
30
|
+
if (this._testifyService) {
|
|
31
|
+
for (const testResult of this._results) {
|
|
32
|
+
await this._testifyService.reportTestResult(testResult.title, testResult.status === 'passed');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (result.status === 'passed' && process.env.AUTO_CLOSE_PASSING_RUNS === 'true')
|
|
37
|
+
await this._testifyService?.closeTestRun();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default ReportingService;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PlaywrightTestArgs,
|
|
3
|
+
PlaywrightTestOptions,
|
|
4
|
+
PlaywrightWorkerArgs,
|
|
5
|
+
PlaywrightWorkerOptions,
|
|
6
|
+
TestType
|
|
7
|
+
} from '@playwright/test';
|
|
8
|
+
|
|
9
|
+
export type Test = TestType<PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type UniversalConfigOptions = {
|
|
2
|
+
barcodeScannerSymbologies?: string;
|
|
3
|
+
cortexEnabledSymbologies?: string[];
|
|
4
|
+
cortexLicense?: string;
|
|
5
|
+
debugMode?: boolean;
|
|
6
|
+
devDisableProcessFlows?: boolean;
|
|
7
|
+
useFormFieldBackgroundColor?: boolean;
|
|
8
|
+
defaultApplication?: string;
|
|
9
|
+
defaultPlatform?: number;
|
|
10
|
+
loginOptionsDisabled?: boolean;
|
|
11
|
+
loginOptionsHidden?: boolean;
|
|
12
|
+
installNotificationDisabled?: boolean;
|
|
13
|
+
enableDragDrop?: boolean;
|
|
14
|
+
installNotificationExpirationInDays?: number;
|
|
15
|
+
spacingMode?: string;
|
|
16
|
+
serviceUrl?: string;
|
|
17
|
+
useServiceWorker?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type UniversalLoginOptions = {
|
|
21
|
+
username?: string;
|
|
22
|
+
password?: string;
|
|
23
|
+
serviceUrl?: string;
|
|
24
|
+
metaEndpoint?: string;
|
|
25
|
+
config?: UniversalConfigOptions;
|
|
26
|
+
};
|