@testomatio/reporter 1.3.6-beta → 1.4.0-beta-csv-enable

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +60 -57
  2. package/lib/adapter/codecept.js +96 -35
  3. package/lib/adapter/cucumber/current.js +19 -12
  4. package/lib/adapter/cucumber/legacy.js +7 -6
  5. package/lib/adapter/cucumber.js +2 -2
  6. package/lib/adapter/cypress-plugin/index.js +53 -26
  7. package/lib/adapter/jasmine.js +2 -2
  8. package/lib/adapter/jest.js +50 -17
  9. package/lib/adapter/mocha.js +110 -58
  10. package/lib/adapter/playwright.js +95 -33
  11. package/lib/adapter/webdriver.js +2 -2
  12. package/lib/bin/reportXml.js +22 -16
  13. package/lib/bin/startTest.js +27 -6
  14. package/lib/client.js +179 -53
  15. package/lib/config.js +34 -0
  16. package/lib/constants.js +32 -7
  17. package/lib/data-storage.js +203 -0
  18. package/lib/fileUploader.js +135 -54
  19. package/lib/junit-adapter/adapter.js +0 -2
  20. package/lib/junit-adapter/csharp.js +3 -4
  21. package/lib/junit-adapter/index.js +3 -3
  22. package/lib/junit-adapter/java.js +35 -17
  23. package/lib/junit-adapter/javascript.js +1 -2
  24. package/lib/junit-adapter/python.js +12 -14
  25. package/lib/junit-adapter/ruby.js +1 -2
  26. package/lib/pipe/csv.js +37 -31
  27. package/lib/pipe/github.js +9 -19
  28. package/lib/pipe/gitlab.js +22 -26
  29. package/lib/pipe/html.js +361 -0
  30. package/lib/pipe/index.js +3 -1
  31. package/lib/pipe/testomatio.js +256 -53
  32. package/lib/reporter-functions.js +46 -0
  33. package/lib/reporter.js +11 -9
  34. package/lib/services/artifacts.js +57 -0
  35. package/lib/services/index.js +13 -0
  36. package/lib/services/key-values.js +58 -0
  37. package/lib/services/logger.js +311 -0
  38. package/lib/template/emptyData.svg +23 -0
  39. package/lib/template/testomatio.hbs +1421 -0
  40. package/lib/utils/pipe_utils.js +129 -0
  41. package/lib/{util.js → utils/utils.js} +147 -17
  42. package/lib/xmlReader.js +211 -122
  43. package/package.json +17 -9
  44. package/lib/_ArtifactStorageOld.js +0 -142
  45. package/lib/artifactStorage.js +0 -25
  46. package/lib/dataStorage.js +0 -244
  47. package/lib/logger.js +0 -301
@@ -1,17 +1,19 @@
1
1
  const { STATUS } = require('../../constants');
2
- const { parseTest, parseSuite } = require('../../util');
2
+ const { getTestomatIdFromTestTitle, parseSuite } = require('../../utils/utils');
3
3
  const TestomatClient = require('../../client');
4
+ const config = require('../../config');
4
5
 
5
6
  const testomatioReporter = on => {
6
- if (!process.env.TESTOMATIO) {
7
+ if (!config.TESTOMATIO) {
7
8
  console.log('TESTOMATIO key is empty, ignoring reports');
8
- return
9
+ return;
9
10
  }
10
- const client = new TestomatClient({ apiKey: process.env.TESTOMATIO });
11
+ const client = new TestomatClient({ apiKey: config.TESTOMATIO });
11
12
 
12
- on('before:run', async (run) => {
13
+ on('before:run', async run => {
14
+ // TODO: looks like client.env does not exist
13
15
  if (!client.env) {
14
- client.env = `${run.browser.displayName},${run.system.osName}`
16
+ client.env = `${run.browser.displayName},${run.system.osName}`;
15
17
  }
16
18
  await client.createRun();
17
19
  });
@@ -25,8 +27,9 @@ const testomatioReporter = on => {
25
27
  const lastAttemptIndex = test.attempts.length - 1;
26
28
  const latestAttempt = test.attempts[lastAttemptIndex];
27
29
 
28
- const time = latestAttempt.duration;
29
- const error = latestAttempt.error;
30
+ // latestAttempt.duration && latestAttempt.error were available in adapters version up to 13 JFYI
31
+ const time = latestAttempt.duration || latestAttempt.wallClockDuration || test.duration;
32
+ let error = latestAttempt.error;
30
33
 
31
34
  let title = test.title.pop();
32
35
  const examples = title.match(/\(example (#\d+)\)/);
@@ -36,38 +39,62 @@ const testomatioReporter = on => {
36
39
 
37
40
  const suiteTitle = test.title.pop();
38
41
 
39
- const testId = parseTest(title);
42
+ const testId = getTestomatIdFromTestTitle(title);
40
43
  const suiteId = parseSuite(suiteTitle);
41
44
 
42
- if (error) {
43
- error.inspect = function() { // eslint-disable-line func-names
44
- if (this && this.codeFrame) {
45
- return this.codeFrame.frame;
46
- }
47
- return '';
48
- }
45
+ if (!error && test.displayError) {
46
+ error = { message: test.displayError };
47
+ error.inspect = function () {
48
+ // eslint-disable-line func-names
49
+ return this.message;
50
+ };
49
51
  }
50
52
 
51
- const screenshots = results.screenshots
52
- .filter(screenshot => screenshot.path.includes(title))
53
- .filter(screenshot => screenshot.testAttemptIndex === lastAttemptIndex)
54
- .map(screenshot => screenshot.path);
53
+ const formattedError = error
54
+ ? {
55
+ message: error.message,
56
+ inspect:
57
+ error.inspect ||
58
+ function () {
59
+ return this.message;
60
+ },
61
+ }
62
+ : '';
63
+
64
+ const screenshots = Array.isArray(results.screenshots)
65
+ ? results.screenshots
66
+ .filter(screenshot => screenshot?.path && screenshot?.path.includes(title) && screenshot?.takenAt)
67
+ .map(screenshot => screenshot.path)
68
+ : [];
55
69
 
56
70
  const files = [...videos, ...screenshots];
57
71
 
58
72
  let state;
59
73
  switch (test.state) {
60
- case 'passed': state = STATUS.PASSED; break;
61
- case 'failed': state = STATUS.FAILED; break;
74
+ case 'passed':
75
+ state = STATUS.PASSED;
76
+ break;
77
+ case 'failed':
78
+ state = STATUS.FAILED;
79
+ break;
62
80
  case 'skipped':
63
- case 'pending':
81
+ case 'pending':
64
82
  default:
65
83
  state = STATUS.SKIPPED;
66
84
  }
67
85
 
68
- addSpecTestsPromises.push(client.addTestRun(state, {
69
- title, time, example, error, files, suite_title: suiteTitle, test_id: testId, suite_id: suiteId
70
- }));
86
+ addSpecTestsPromises.push(
87
+ client.addTestRun(state, {
88
+ title,
89
+ time,
90
+ example,
91
+ error: formattedError,
92
+ files,
93
+ suite_title: suiteTitle,
94
+ test_id: testId,
95
+ suite_id: suiteId,
96
+ }),
97
+ );
71
98
  }
72
99
 
73
100
  await Promise.all(addSpecTestsPromises);
@@ -1,5 +1,5 @@
1
1
  const TestomatClient = require('../client');
2
- const { parseTest, ansiRegExp } = require('../util');
2
+ const { getTestomatIdFromTestTitle, ansiRegExp } = require('../utils/utils');
3
3
  const { STATUS } = require('../constants');
4
4
 
5
5
  class JasmineReporter {
@@ -34,7 +34,7 @@ class JasmineReporter {
34
34
  }
35
35
  console.log(`${title} : ${STATUS.PASSED}`);
36
36
  console.log(errorMessage);
37
- const testId = parseTest(title);
37
+ const testId = getTestomatIdFromTestTitle(title);
38
38
  errorMessage = errorMessage.replace(ansiRegExp(), '');
39
39
  this.client.addTestRun(status, {
40
40
  error: result.failedExpectations[0],
@@ -1,6 +1,10 @@
1
+ const chalk = require('chalk');
1
2
  const TestomatClient = require('../client');
2
- const { STATUS, TESTOMAT_TMP_STORAGE } = require('../constants');
3
- const { parseTest, ansiRegExp, fileSystem } = require('../util');
3
+ const { STATUS, TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
4
+ const { getTestomatIdFromTestTitle, ansiRegExp, fileSystem } = require('../utils/utils');
5
+ const { services } = require('../services');
6
+ const debug = require('debug')('@testomatio/reporter:adapter-jest');
7
+ const path = require('path');
4
8
 
5
9
  class JestReporter {
6
10
  constructor(globalConfig, options) {
@@ -11,22 +15,24 @@ class JestReporter {
11
15
  this.client.createRun();
12
16
  }
13
17
 
14
- static getIdOfCurrentlyRunningTest() {
15
- if (!process.env.JEST_WORKER_ID) return null;
16
- try {
17
- // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
18
- // eslint-disable-next-line no-undef
19
- if (expect && expect?.getState()?.currentTestName) return parseTest(expect?.getState()?.currentTestName);
20
- } catch (e) {
21
- return null;
22
- }
23
- }
24
-
25
18
  onRunStart() {
26
19
  // clear tmp dir
27
- fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
20
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
21
+ }
22
+
23
+ // start of test file (including beforeAll)
24
+ onTestStart(testFile) {
25
+ debug('Start running test file:', testFile.path);
26
+ services.setContext(testFile.path);
27
+ }
28
+
29
+ // start of the test (including beforeEach)
30
+ onTestCaseStart(test, testCase) {
31
+ debug('Start running test:', testCase.fullName);
32
+ services.setContext(testCase.fullName);
28
33
  }
29
34
 
35
+ // end of test file! (there is also onTestCaseResult listener)
30
36
  onTestResult(test, testResult) {
31
37
  if (!this.client) return;
32
38
 
@@ -41,17 +47,31 @@ class JestReporter {
41
47
  error = new Error(errorMessage);
42
48
  steps = failureMessages[0];
43
49
  }
44
- const testId = parseTest(title);
50
+ const testId = getTestomatIdFromTestTitle(title);
51
+
52
+ // suite titles from most outer to most inner, separated by space
53
+ let fullSuiteTitle = testResult.ancestorTitles?.join(' ');
54
+ // if no suite titles, use file name
55
+ if (!fullSuiteTitle && testResult.testFilePath) fullSuiteTitle = path.basename(testResult.testFilePath);
56
+
57
+ const logs = getTestLogs(result);
58
+ const artifacts = services.artifacts.get(result.fullName);
59
+ const keyValues = services.keyValues.get(result.fullName);
60
+
45
61
  const deducedStatus = status === 'pending' ? 'skipped' : status;
46
62
  // In jest if test is not matched with test name pattern it is considered as skipped.
47
63
  // So adding a check if it is skipped for real or because of test pattern
48
64
  if (!this._globalConfig.testNamePattern || deducedStatus !== 'skipped') {
49
65
  this.client.addTestRun(deducedStatus, {
50
66
  test_id: testId,
67
+ suite_title: fullSuiteTitle,
51
68
  error,
52
69
  steps,
53
70
  title,
54
71
  time: duration,
72
+ logs,
73
+ manuallyAttachedArtifacts: artifacts,
74
+ meta: keyValues,
55
75
  });
56
76
  }
57
77
  }
@@ -63,10 +83,23 @@ class JestReporter {
63
83
  const { numFailedTests } = results;
64
84
  const status = numFailedTests === 0 ? STATUS.PASSED : STATUS.FAILED;
65
85
  this.client.updateRunStatus(status);
86
+ }
87
+ }
66
88
 
67
- // clear tmp dir
68
- fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
89
+ function getTestLogs(testResult) {
90
+ const suiteLogsArr = services.logger.getLogs(testResult.testFilePath);
91
+ const suiteLogs = suiteLogsArr ? suiteLogsArr.join('\n').trim() : '';
92
+ const testLogsArr = services.logger.getLogs(testResult.fullName);
93
+ const testLogs = testLogsArr ? testLogsArr.join('\n').trim() : '';
94
+
95
+ let logs = '';
96
+ if (suiteLogs) {
97
+ logs += `${chalk.bold('\t--- Suite ---')}\n${suiteLogs}`;
98
+ }
99
+ if (testLogs) {
100
+ logs += `\n${chalk.bold('\t--- Test ---')}\n${testLogs}`;
69
101
  }
102
+ return logs;
70
103
  }
71
104
 
72
105
  module.exports = JestReporter;
@@ -1,102 +1,154 @@
1
1
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies
2
2
  const Mocha = require('mocha');
3
- const debug = require('debug')('@testomatio/reporter:adapter:mocha');
4
3
  const chalk = require('chalk');
5
4
  const TestomatClient = require('../client');
6
- const { STATUS } = require('../constants');
7
- const { parseTest, specificTestInfo } = require('../util');
8
- const ArtifactStorage = require('../_ArtifactStorageOld');
9
-
10
- const { EVENT_RUN_BEGIN, EVENT_RUN_END, EVENT_TEST_FAIL, EVENT_TEST_PASS, EVENT_TEST_PENDING } = Mocha.Runner.constants;
5
+ const { STATUS, TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
6
+ const { getTestomatIdFromTestTitle, fileSystem } = require('../utils/utils');
7
+ const config = require('../config');
8
+ const { services } = require('../services');
9
+
10
+ const {
11
+ EVENT_RUN_BEGIN,
12
+ EVENT_RUN_END,
13
+ EVENT_TEST_FAIL,
14
+ EVENT_TEST_PASS,
15
+ EVENT_TEST_PENDING,
16
+ EVENT_SUITE_BEGIN,
17
+ EVENT_SUITE_END,
18
+ EVENT_TEST_BEGIN,
19
+ EVENT_TEST_END,
20
+ } = Mocha.Runner.constants;
11
21
 
12
22
  function MochaReporter(runner, opts) {
13
23
  Mocha.reporters.Base.call(this, runner);
14
- let passes = 0; let failures = 0; let skipped = 0;
15
- let artifactStore;
24
+ let passes = 0;
25
+ let failures = 0;
26
+ let skipped = 0;
27
+ // let artifactStore;
16
28
 
17
- const apiKey = opts?.reporterOptions?.apiKey || process.env.TESTOMATIO;
29
+ const apiKey = opts?.reporterOptions?.apiKey || config.TESTOMATIO;
18
30
 
19
- if (!apiKey) {
20
- debug('TESTOMATIO key is empty, ignoring reports');
21
- return;
22
- }
23
31
  const client = new TestomatClient({ apiKey });
24
32
 
25
33
  runner.on(EVENT_RUN_BEGIN, () => {
26
34
  client.createRun();
27
35
 
28
- const params = {
29
- toFile: true
30
- };
31
-
32
- artifactStore = runner._workerReporter !== undefined
33
- ? new ArtifactStorage(params)
34
- : new ArtifactStorage();
36
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
37
+ });
38
+
39
+ runner.on(EVENT_SUITE_BEGIN, async suite => {
40
+ services.setContext(suite.fullTitle());
41
+ });
42
+
43
+ runner.on(EVENT_SUITE_END, async () => {
44
+ services.setContext(null);
45
+ });
46
+
47
+ runner.on(EVENT_TEST_BEGIN, async test => {
48
+ services.setContext(test.fullTitle());
49
+ });
50
+
51
+ runner.on(EVENT_TEST_END, async () => {
52
+ services.setContext(null);
35
53
  });
36
54
 
37
- runner.on(EVENT_TEST_PASS, async(test) => {
55
+ runner.on(EVENT_TEST_PASS, async test => {
38
56
  passes += 1;
57
+
39
58
  console.log(chalk.bold.green('✔'), test.fullTitle());
40
- const testId = parseTest(test.title);
41
-
42
- const specificTest = specificTestInfo(test);
43
- const content = await artifactStore.artifactByTestName(specificTest);
44
-
45
- debug(`test=${specificTest} content = `, content);
46
-
47
- client.addTestRun(
48
- STATUS.PASSED,
49
- {
50
- test_id: testId,
51
- title: test.title,
52
- time: test.duration,
53
- },
54
- content
55
- );
59
+ const testId = getTestomatIdFromTestTitle(test.title);
60
+
61
+ const logs = getTestLogs(test);
62
+ const artifacts = services.artifacts.get(test.fullTitle());
63
+ const keyValues = services.keyValues.get(test.fullTitle());
64
+
65
+ client.addTestRun(STATUS.PASSED, {
66
+ test_id: testId,
67
+ suite_title: getSuiteTitle(test),
68
+ title: getTestName(test),
69
+ code: test.body.toString(),
70
+ file: getFile(test),
71
+ time: test.duration,
72
+ logs,
73
+ manuallyAttachedArtifacts: artifacts,
74
+ meta: keyValues,
75
+ });
56
76
  });
57
77
 
58
78
  runner.on(EVENT_TEST_PENDING, test => {
59
79
  skipped += 1;
60
80
  console.log('skip: %s', test.fullTitle());
61
- const testId = parseTest(test.title);
81
+ const testId = getTestomatIdFromTestTitle(test.title);
62
82
  client.addTestRun(STATUS.SKIPPED, {
63
- title: test.title,
83
+ title: getTestName(test),
84
+ suite_title: getSuiteTitle(test),
85
+ code: test.body.toString(),
86
+ file: getFile(test),
64
87
  test_id: testId,
65
88
  time: test.duration,
66
89
  });
67
90
  });
68
91
 
69
- runner.on(EVENT_TEST_FAIL, async(test, err) => {
92
+ runner.on(EVENT_TEST_FAIL, async (test, err) => {
70
93
  failures += 1;
71
94
  console.log(chalk.bold.red('✖'), test.fullTitle(), chalk.gray(err.message));
72
- const testId = parseTest(test.title);
73
-
74
- const specificTest = specificTestInfo(test);
75
- const content = await artifactStore.artifactByTestName(specificTest);
76
-
77
- debug(`fail test=${specificTest} content = `, content);
78
-
79
- client.addTestRun(
80
- STATUS.FAILED, {
81
- error: err,
82
- test_id: testId,
83
- title: test.title,
84
- time: test.duration,
85
- },
86
- content
87
- );
95
+ const testId = getTestomatIdFromTestTitle(test.title);
96
+
97
+ const logs = getTestLogs(test);
98
+
99
+ client.addTestRun(STATUS.FAILED, {
100
+ error: err,
101
+ suite_title: getSuiteTitle(test),
102
+ file: getFile(test),
103
+ test_id: testId,
104
+ title: getTestName(test),
105
+ code: test.body.toString(),
106
+ time: test.duration,
107
+ logs,
108
+ });
88
109
  });
89
110
 
90
111
  runner.on(EVENT_RUN_END, () => {
91
112
  const status = failures === 0 ? STATUS.PASSED : STATUS.FAILED;
92
113
  console.log(chalk.bold(status), `${passes} passed, ${failures} failed, ${skipped} skipped`);
93
114
  client.updateRunStatus(status);
94
-
95
- artifactStore.cleanup();
96
115
  });
97
116
  }
98
117
 
118
+ function getTestLogs(test) {
119
+ const suiteLogsArr = services.logger.getLogs(test.parent.fullTitle());
120
+ const suiteLogs = suiteLogsArr ? suiteLogsArr.join('\n').trim() : '';
121
+ const testLogsArr = services.logger.getLogs(test.fullTitle());
122
+ const testLogs = testLogsArr ? testLogsArr.join('\n').trim() : '';
123
+
124
+ let logs = '';
125
+ if (suiteLogs) {
126
+ logs += `${chalk.bold('\t--- BeforeSuite ---')}\n${suiteLogs}`;
127
+ }
128
+ if (testLogs) {
129
+ logs += `\n${chalk.bold('\t--- Test ---')}\n${testLogs}`;
130
+ }
131
+ return logs;
132
+ }
133
+
99
134
  // To have this reporter "extend" a built-in reporter uncomment the following line:
100
135
  Mocha.utils.inherits(MochaReporter, Mocha.reporters.Spec);
101
136
 
102
137
  module.exports = MochaReporter;
138
+
139
+ function getSuiteTitle(test, pathArr = []) {
140
+ if (test.parent.parent) getSuiteTitle(test.parent, pathArr);
141
+
142
+ pathArr.push(test.parent.title);
143
+
144
+ return pathArr.filter(t => !!t)[0];
145
+ }
146
+
147
+ function getFile(test) {
148
+ return test.parent.file?.replace(process.cwd(), '');
149
+ }
150
+
151
+ function getTestName(test) {
152
+ if (process.env.TESTOMATIO_CREATE === 'fulltitle') return test.fullTitle();
153
+ return test.title;
154
+ }
@@ -3,10 +3,13 @@ const crypto = require('crypto');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
- const { APP_PREFIX, STATUS: Status, TESTOMAT_TMP_STORAGE } = require('../constants');
6
+ const { APP_PREFIX, STATUS: Status, TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
7
7
  const TestomatioClient = require('../client');
8
8
  const { isArtifactsEnabled } = require('../fileUploader');
9
- const { parseTest, fileSystem } = require('../util');
9
+ const { getTestomatIdFromTestTitle, fileSystem } = require('../utils/utils');
10
+ // const debug = require('debug')('@testomatio/reporter:adapter:playwright');
11
+ const { services } = require('../services');
12
+ const { dataStorage } = require('../data-storage');
10
13
 
11
14
  const reportTestPromises = [];
12
15
 
@@ -17,54 +20,90 @@ class PlaywrightReporter {
17
20
  this.uploads = [];
18
21
  }
19
22
 
20
- onBegin(_config, suite) {
23
+ onBegin(config, suite) {
21
24
  // clean data storage
22
- fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
25
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
23
26
  if (!this.client) return;
24
27
  this.suite = suite;
28
+ this.config = config;
25
29
  this.client.createRun();
26
30
  }
27
31
 
28
- // onTestBegin(test) {
29
- // const testId = parseTest(test.title);
30
- // }
32
+ onTestBegin(testInfo) {
33
+ const fullTestTitle = getTestContextName(testInfo);
34
+ dataStorage.setContext(fullTestTitle);
35
+ }
31
36
 
32
37
  onTestEnd(test, result) {
33
38
  if (!this.client) return;
34
39
 
35
- let testId = parseTest(test.title);
36
-
37
40
  const { title } = test;
41
+
42
+ let testId = getTestomatIdFromTestTitle(`${title} ${test.tags?.join(' ')}`);
43
+
38
44
  const { error, duration } = result;
39
45
 
40
- const suite_title = test.parent ? test.parent.title : null;
46
+ const suite_title = test.parent ? test.parent?.title : path.basename(test?.location?.file);
41
47
 
42
48
  const steps = [];
43
49
  for (const step of result.steps) {
44
50
  appendStep(step, steps);
45
51
  }
46
52
 
47
- const logs = `\n\n${chalk.bold('Logs:')}\n${chalk.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
48
-
49
- const reportTestPromise = this.client.addTestRun(checkStatus(result.status), {
50
- error,
51
- test_id: testId,
52
- suite_title,
53
- title,
54
- steps: steps.join('\n'),
55
- time: duration,
56
- stack: logs,
57
- }).then(pipes => {
58
- testId = pipes?.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
59
-
60
- this.uploads.push({
61
- testId, title, suite_title, files: result.attachments.filter((a) => a.body || a.path)
53
+ const fullTestTitle = getTestContextName(test);
54
+ let logs = '';
55
+ if (result.stderr.length || result.stdout.length) {
56
+ logs = `\n\n${chalk.bold('Logs:')}\n${chalk.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
57
+ }
58
+ const manuallyAttachedArtifacts = services.artifacts.get(fullTestTitle);
59
+ const keyValues = services.keyValues.get(fullTestTitle);
60
+
61
+ const reportTestPromise = this.client
62
+ .addTestRun(checkStatus(result.status), {
63
+ error,
64
+ test_id: testId,
65
+ suite_title,
66
+ title,
67
+ steps: steps.join('\n'),
68
+ time: duration,
69
+ logs,
70
+ manuallyAttachedArtifacts,
71
+ meta: keyValues,
72
+ file: test.location?.file,
73
+ })
74
+ .then(pipes => {
75
+ testId = pipes?.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
76
+
77
+ this.uploads.push({
78
+ testId,
79
+ title,
80
+ suite_title,
81
+ files: result.attachments.filter(a => a.body || a.path),
82
+ file: test.location?.file,
83
+ });
84
+ // remove empty uploads
85
+ this.uploads = this.uploads.filter(upload => upload.files.length);
62
86
  });
63
- });
64
87
 
65
88
  reportTestPromises.push(reportTestPromise);
66
89
  }
67
90
 
91
+ #getArtifactPath(artifact) {
92
+ if (artifact.path) {
93
+ if (path.isAbsolute(artifact.path)) return artifact.path;
94
+
95
+ return path.join(this.config.outputDir || this.config.projects[0].outputDir, artifact.path);
96
+ }
97
+
98
+ if (artifact.body) {
99
+ const fileName = tmpFile();
100
+ fs.writeFileSync(fileName, artifact.body);
101
+ return fileName;
102
+ }
103
+
104
+ return null;
105
+ }
106
+
68
107
  async onEnd(result) {
69
108
  if (!this.client) return;
70
109
 
@@ -78,13 +117,11 @@ class PlaywrightReporter {
78
117
  for (const upload of this.uploads) {
79
118
  const { title, testId, suite_title } = upload;
80
119
 
81
- const files = upload.files.map(attachment => {
82
- if (attachment.body) {
83
- const fileName = tmpFile();
84
- fs.writeFileSync(fileName, attachment.body);
85
- }
86
- return { path: attachment.path, title, type: attachment.contentType };
87
- });
120
+ const files = upload.files.map(attachment => ({
121
+ path: this.#getArtifactPath(attachment),
122
+ title,
123
+ type: attachment.contentType,
124
+ }));
88
125
 
89
126
  promises.push(
90
127
  this.client.addTestRun(undefined, {
@@ -92,6 +129,7 @@ class PlaywrightReporter {
92
129
  title,
93
130
  suite_title,
94
131
  files,
132
+ file: upload.file,
95
133
  }),
96
134
  );
97
135
  }
@@ -131,4 +169,28 @@ function tmpFile(prefix = 'tmp.') {
131
169
  return path.join(tmpdir, prefix + crypto.randomBytes(16).toString('hex'));
132
170
  }
133
171
 
172
+ /**
173
+ * Returns filename + test title
174
+ * @param {*} test - testInfo object from Playwright
175
+ * @returns
176
+ */
177
+ function getTestContextName(test) {
178
+ return `${test._requireFile || ''}_${test.title}`;
179
+ }
180
+
181
+ function initPlaywrightForStorage() {
182
+ try {
183
+ // @ts-ignore-next-line
184
+ // eslint-disable-next-line import/no-unresolved
185
+ const { test } = require('@playwright/test');
186
+ // eslint-disable-next-line no-empty-pattern
187
+ test.beforeEach(async ({}, testInfo) => {
188
+ global.testomatioTestTitle = `${testInfo.file || ''}_${testInfo.title}`;
189
+ });
190
+ } catch (e) {
191
+ // ignore
192
+ }
193
+ }
194
+
134
195
  module.exports = PlaywrightReporter;
196
+ module.exports.initPlaywrightForStorage = initPlaywrightForStorage;
@@ -1,7 +1,7 @@
1
1
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies
2
2
  const WDIOReporter = require('@wdio/reporter').default;
3
3
  const TestomatClient = require('../client');
4
- const { parseTest } = require('../util');
4
+ const { getTestomatIdFromTestTitle } = require('../utils/utils');
5
5
 
6
6
  class WebdriverReporter extends WDIOReporter {
7
7
  constructor(options) {
@@ -36,7 +36,7 @@ class WebdriverReporter extends WDIOReporter {
36
36
 
37
37
  const { title, _duration: duration, state, error, output } = test;
38
38
 
39
- const testId = parseTest(title);
39
+ const testId = getTestomatIdFromTestTitle(title);
40
40
 
41
41
  const screenshotEndpoint = '/session/:sessionId/screenshot';
42
42
  const screenshotsBuffers = output