artes 1.4.5 → 1.4.6
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/cucumber.config.js +2 -2
- package/executer.js +153 -87
- package/package.json +1 -2
- package/src/helper/executers/reportGenerator.js +1 -1
- package/src/helper/imports/commons.js +1 -1
- package/src/hooks/hooks.js +0 -24
- package/status-formatter.js +138 -0
package/cucumber.config.js
CHANGED
|
@@ -14,7 +14,7 @@ try {
|
|
|
14
14
|
console.log("Proceeding with default config.");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const defaultFormats = ["rerun:@rerun.txt",
|
|
17
|
+
const defaultFormats = ["rerun:@rerun.txt", "progress-bar", './status-formatter.js:null'];
|
|
18
18
|
|
|
19
19
|
const userFormatsFromEnv = process.env.REPORT_FORMAT
|
|
20
20
|
? JSON.parse(process.env.REPORT_FORMAT)
|
|
@@ -55,7 +55,7 @@ function loadVariables(cliVariables, artesConfigVariables) {
|
|
|
55
55
|
|
|
56
56
|
if (cliVariables) {
|
|
57
57
|
try {
|
|
58
|
-
cliVariables = JSON.parse(
|
|
58
|
+
cliVariables = JSON.parse(cliVariables);
|
|
59
59
|
} catch (err) {
|
|
60
60
|
console.error("Invalid JSON in process.env.VARS:", process.env.VARS);
|
|
61
61
|
envVars = {};
|
package/executer.js
CHANGED
|
@@ -22,6 +22,11 @@ if (fs.existsSync(artesConfigPath)) {
|
|
|
22
22
|
|
|
23
23
|
const args = process.argv.slice(2);
|
|
24
24
|
|
|
25
|
+
const getArgValue = (flag) => {
|
|
26
|
+
const index = args.indexOf(flag);
|
|
27
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
25
30
|
const flags = {
|
|
26
31
|
help: args.includes("-h") || args.includes("--help"),
|
|
27
32
|
version: args.includes("-v") || args.includes("--version"),
|
|
@@ -56,23 +61,25 @@ const flags = {
|
|
|
56
61
|
slowMo: args.includes("--slowMo"),
|
|
57
62
|
};
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
64
|
+
|
|
65
|
+
const env = getArgValue("--env");
|
|
66
|
+
const vars = getArgValue("--saveVar");
|
|
67
|
+
const featureFiles = getArgValue("--features");
|
|
62
68
|
const features = flags.features && featureFiles;
|
|
63
|
-
const stepDef =
|
|
64
|
-
const tags =
|
|
65
|
-
const parallel =
|
|
66
|
-
const retry =
|
|
67
|
-
const rerun =
|
|
68
|
-
const percentage =
|
|
69
|
-
const browser =
|
|
70
|
-
const device =
|
|
71
|
-
const baseURL =
|
|
72
|
-
const width =
|
|
73
|
-
const height =
|
|
74
|
-
const timeout =
|
|
75
|
-
const slowMo =
|
|
69
|
+
const stepDef = getArgValue("--stepDef");
|
|
70
|
+
const tags = getArgValue("--tags");
|
|
71
|
+
const parallel = getArgValue("--parallel");
|
|
72
|
+
const retry = getArgValue("--retry");
|
|
73
|
+
const rerun = getArgValue("--rerun");
|
|
74
|
+
const percentage = getArgValue("--percentage");
|
|
75
|
+
const browser = getArgValue("--browser");
|
|
76
|
+
const device = getArgValue("--device");
|
|
77
|
+
const baseURL = getArgValue("--baseURL");
|
|
78
|
+
const width = getArgValue("--width");
|
|
79
|
+
const height = getArgValue("--height");
|
|
80
|
+
const timeout = getArgValue("--timeout");
|
|
81
|
+
const slowMo = getArgValue("--slowMo");
|
|
82
|
+
|
|
76
83
|
|
|
77
84
|
flags.env ? (process.env.ENV = env) : "";
|
|
78
85
|
|
|
@@ -152,80 +159,137 @@ flags.timeout ? (process.env.TIMEOUT = timeout) : "";
|
|
|
152
159
|
flags.slowMo ? (process.env.SLOWMO = slowMo) : "";
|
|
153
160
|
|
|
154
161
|
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
function findDuplicateTestNames() {
|
|
163
|
+
const testStatusFile = path.join(process.cwd(), "node_modules", "artes" , "test-status", 'test-status.txt');
|
|
164
|
+
|
|
165
|
+
if (!fs.existsSync(testStatusFile)) {
|
|
166
|
+
console.error('test-status.txt not found');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const content = fs.readFileSync(testStatusFile, 'utf8');
|
|
171
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
172
|
+
|
|
173
|
+
const testNameToFiles = {};
|
|
174
|
+
|
|
175
|
+
lines.forEach(line => {
|
|
176
|
+
const parts = line.split(' | ');
|
|
177
|
+
if (parts.length < 5) return;
|
|
178
|
+
|
|
179
|
+
const testName = parts[2].trim();
|
|
180
|
+
const filePath = parts[4].trim();
|
|
181
|
+
|
|
182
|
+
if (!testNameToFiles[testName]) {
|
|
183
|
+
testNameToFiles[testName] = new Set();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
testNameToFiles[testName].add(filePath);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const duplicates = {};
|
|
190
|
+
|
|
191
|
+
Object.entries(testNameToFiles).forEach(([testName, files]) => {
|
|
192
|
+
if (files.size > 1) {
|
|
193
|
+
duplicates[testName] = Array.from(files);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (Object.keys(duplicates).length > 0) {
|
|
198
|
+
console.warn('\n\x1b[33m[WARNING] Duplicate scenarios names found: This will effect your reporting');
|
|
199
|
+
Object.entries(duplicates).forEach(([testName, files]) => {
|
|
200
|
+
console.log(`\x1b[33m"${testName}" exists in:`);
|
|
201
|
+
files.forEach(file => {
|
|
202
|
+
console.log(` - ${file}`);
|
|
203
|
+
});
|
|
204
|
+
console.log('');
|
|
205
|
+
});
|
|
206
|
+
console.log("\x1b[0m");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return duplicates;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
function testCoverageCalculation() {
|
|
158
214
|
|
|
159
|
-
|
|
160
|
-
const retriedTests = [];
|
|
161
|
-
const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
215
|
+
const testStatusFile = path.join(process.cwd(), "node_modules", "artes" , "test-status", 'test-status.txt');
|
|
162
216
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
217
|
+
if (!fs.existsSync(testStatusFile)) {
|
|
218
|
+
console.error('test-status.txt not found');
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const content = fs.readFileSync(testStatusFile, 'utf8');
|
|
223
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
224
|
+
|
|
225
|
+
const map = {};
|
|
226
|
+
const retriedTests = [];
|
|
227
|
+
const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
228
|
+
|
|
229
|
+
lines.forEach(line => {
|
|
230
|
+
const parts = line.split(' | ');
|
|
231
|
+
if (parts.length < 5) return;
|
|
232
|
+
|
|
233
|
+
const timestamp = parts[0].trim();
|
|
234
|
+
const status = parts[1].trim();
|
|
235
|
+
const scenario = parts[2].trim();
|
|
236
|
+
const id = parts[3].trim();
|
|
237
|
+
const uri = parts[4].trim();
|
|
238
|
+
|
|
239
|
+
if (!uuidRegex.test(id)) return;
|
|
240
|
+
|
|
241
|
+
if (!map[id]) {
|
|
242
|
+
map[id] = {
|
|
243
|
+
count: 1,
|
|
244
|
+
latest: { status, scenario, timestamp, uri }
|
|
245
|
+
};
|
|
246
|
+
} else {
|
|
247
|
+
map[id].count++;
|
|
248
|
+
if (timestamp > map[id].latest.timestamp) {
|
|
249
|
+
map[id].latest = { status, scenario, timestamp, uri };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
let total = 0;
|
|
255
|
+
let notPassed = 0;
|
|
256
|
+
|
|
257
|
+
Object.entries(map).forEach(([id, data]) => {
|
|
258
|
+
total++;
|
|
259
|
+
|
|
260
|
+
if (data.count > 1) {
|
|
261
|
+
retriedTests.push({
|
|
262
|
+
scenario: data.latest.scenario,
|
|
263
|
+
id,
|
|
264
|
+
count: data.count
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (data.latest.status !== 'PASSED') {
|
|
204
269
|
notPassed++;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
percentage : (total - notPassed)/total*100,
|
|
218
|
-
totalTests: total,
|
|
219
|
-
notPassed,
|
|
220
|
-
passed: total - notPassed,
|
|
221
|
-
latestStatuses: Object.fromEntries(
|
|
222
|
-
Object.entries(map).map(([id, data]) => [
|
|
223
|
-
id,
|
|
224
|
-
data.latest.status
|
|
225
|
-
])
|
|
226
|
-
)
|
|
227
|
-
};
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (retriedTests.length > 0) {
|
|
274
|
+
console.warn('\n\x1b[33mRetried test cases:');
|
|
275
|
+
retriedTests.forEach(t => {
|
|
276
|
+
console.warn(`- "${t.scenario}" ran ${t.count} times`);
|
|
277
|
+
});
|
|
278
|
+
console.log("\x1b[0m");
|
|
228
279
|
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
percentage: (total - notPassed) / total * 100,
|
|
283
|
+
totalTests: total,
|
|
284
|
+
notPassed,
|
|
285
|
+
passed: total - notPassed,
|
|
286
|
+
latestStatuses: Object.fromEntries(
|
|
287
|
+
Object.entries(map).map(([id, data]) => [
|
|
288
|
+
id,
|
|
289
|
+
data.latest.status
|
|
290
|
+
])
|
|
291
|
+
)
|
|
292
|
+
};
|
|
229
293
|
}
|
|
230
294
|
|
|
231
295
|
function main() {
|
|
@@ -237,7 +301,9 @@ function main() {
|
|
|
237
301
|
|
|
238
302
|
logPomWarnings();
|
|
239
303
|
|
|
240
|
-
|
|
304
|
+
findDuplicateTestNames();
|
|
305
|
+
|
|
306
|
+
const testCoverage = testCoverageCalculation()
|
|
241
307
|
|
|
242
308
|
const testPercentage = (process.env.PERCENTAGE ? Number(process.env.PERCENTAGE) : artesConfig.testPercentage || 0)
|
|
243
309
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "artes",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"description": "The simplest way to automate UI and API tests using Cucumber-style steps.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"dayjs": "1.11.13",
|
|
31
31
|
"deasync": "^0.1.31",
|
|
32
32
|
"playwright": "^1.58.2",
|
|
33
|
-
"proper-lockfile": "^4.1.2",
|
|
34
33
|
"rimraf": "6.0.1"
|
|
35
34
|
},
|
|
36
35
|
"repository": {
|
|
@@ -30,7 +30,7 @@ function generateReport() {
|
|
|
30
30
|
);
|
|
31
31
|
|
|
32
32
|
if (fs.existsSync(moduleConfig.reportPath) && process.env.ZIP === "true") {
|
|
33
|
-
console.log(`🗜️ Zipping report folder
|
|
33
|
+
console.log(`🗜️ Zipping report folder...`);
|
|
34
34
|
|
|
35
35
|
const zipPath = path.join(
|
|
36
36
|
path.dirname(moduleConfig.reportPath),
|
|
@@ -35,7 +35,7 @@ const moduleConfig = {
|
|
|
35
35
|
stepsPath: path.join(projectPath, "/tests/steps/*.js"),
|
|
36
36
|
pomPath: path.join(projectPath, "/tests/POMs"),
|
|
37
37
|
cleanUpPaths:
|
|
38
|
-
"allure-result allure-results test-results @rerun.txt
|
|
38
|
+
"allure-result allure-results test-results @rerun.txt test-status null pomDuplicateWarnings.json",
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
module.exports = {
|
package/src/hooks/hooks.js
CHANGED
|
@@ -41,29 +41,6 @@ async function attachResponse(attachFn) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function saveTestStatus(result, pickle) {
|
|
45
|
-
|
|
46
|
-
fs.mkdirSync(statusDir, { recursive: true });
|
|
47
|
-
|
|
48
|
-
const now = new Date();
|
|
49
|
-
const YYYY = now.getFullYear();
|
|
50
|
-
const MM = String(now.getMonth() + 1).padStart(2, '0');
|
|
51
|
-
const DD = String(now.getDate()).padStart(2, '0');
|
|
52
|
-
const hh = String(now.getHours()).padStart(2, '0');
|
|
53
|
-
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
54
|
-
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
55
|
-
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
56
|
-
|
|
57
|
-
const timestamp = `${YYYY}${MM}${DD}-${hh}${mm}${ss}-${ms}`;
|
|
58
|
-
|
|
59
|
-
const fileName = `${result.status}-${pickle.name}-${pickle.id}-${timestamp}.txt`;
|
|
60
|
-
|
|
61
|
-
const filePath = path.join(statusDir, fileName);
|
|
62
|
-
|
|
63
|
-
fs.writeFileSync(filePath, "");
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
44
|
const projectHooksPath = path.resolve(
|
|
68
45
|
moduleConfig.projectPath,
|
|
69
46
|
"tests/steps/hooks.js",
|
|
@@ -178,7 +155,6 @@ After(async function ({result, pickle}) {
|
|
|
178
155
|
await allure.attachment("Screenshot", screenshotBuffer, "image/png");
|
|
179
156
|
}
|
|
180
157
|
|
|
181
|
-
if(cucumberConfig.default.testPercentage>0) saveTestStatus(result, pickle);
|
|
182
158
|
|
|
183
159
|
if (cucumberConfig.default.reportWithTrace || cucumberConfig.default.trace) {
|
|
184
160
|
var tracePath = path.join(
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const { Formatter } = require('@cucumber/cucumber');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
class StatusFormatter extends Formatter {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
super(options);
|
|
8
|
+
|
|
9
|
+
const outputDir = './test-status';
|
|
10
|
+
if (!fs.existsSync(outputDir)) {
|
|
11
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const workerId = process.env.CUCUMBER_WORKER_ID || '0';
|
|
15
|
+
this.workerId = workerId;
|
|
16
|
+
this.outputFile = path.join(outputDir, `test-results-${workerId}.txt`);
|
|
17
|
+
this.outputDir = outputDir;
|
|
18
|
+
|
|
19
|
+
fs.writeFileSync(this.outputFile, '', 'utf8');
|
|
20
|
+
|
|
21
|
+
this.pickles = new Map();
|
|
22
|
+
this.testCases = new Map();
|
|
23
|
+
this.testCaseStartedIdToAttempt = new Map();
|
|
24
|
+
|
|
25
|
+
const { eventBroadcaster } = options;
|
|
26
|
+
|
|
27
|
+
eventBroadcaster.on('envelope', (envelope) => {
|
|
28
|
+
if (envelope.pickle) {
|
|
29
|
+
this.pickles.set(envelope.pickle.id, envelope.pickle);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (envelope.testCase) {
|
|
33
|
+
this.testCases.set(envelope.testCase.id, envelope.testCase);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (envelope.testCaseStarted) {
|
|
37
|
+
this.testCaseStartedIdToAttempt.set(
|
|
38
|
+
envelope.testCaseStarted.id,
|
|
39
|
+
envelope.testCaseStarted.testCaseId
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (envelope.testCaseFinished) {
|
|
44
|
+
this.handleTestCaseFinished(envelope.testCaseFinished);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (envelope.testRunFinished) {
|
|
48
|
+
this.handleTestRunFinished();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getFormattedTimestamp() {
|
|
54
|
+
const now = new Date();
|
|
55
|
+
const YYYY = now.getFullYear();
|
|
56
|
+
const MM = String(now.getMonth() + 1).padStart(2, '0');
|
|
57
|
+
const DD = String(now.getDate()).padStart(2, '0');
|
|
58
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
59
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
60
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
61
|
+
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
62
|
+
|
|
63
|
+
return `${YYYY}${MM}${DD}-${hh}${mm}${ss}-${ms}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
handleTestCaseFinished(testCaseFinished) {
|
|
67
|
+
try {
|
|
68
|
+
const timestamp = this.getFormattedTimestamp();
|
|
69
|
+
|
|
70
|
+
const testCaseId = this.testCaseStartedIdToAttempt.get(
|
|
71
|
+
testCaseFinished.testCaseStartedId
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!testCaseId) return;
|
|
75
|
+
|
|
76
|
+
const testCase = this.testCases.get(testCaseId);
|
|
77
|
+
if (!testCase) return;
|
|
78
|
+
|
|
79
|
+
const pickle = this.pickles.get(testCase.pickleId);
|
|
80
|
+
if (!pickle) return;
|
|
81
|
+
|
|
82
|
+
const testCaseAttempt = this.eventDataCollector.getTestCaseAttempt(
|
|
83
|
+
testCaseFinished.testCaseStartedId
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const status = testCaseAttempt?.worstTestStepResult?.status || 'UNKNOWN';
|
|
87
|
+
|
|
88
|
+
const info = {
|
|
89
|
+
name: pickle.name,
|
|
90
|
+
id: pickle.id,
|
|
91
|
+
uri: pickle.uri
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const line = `${timestamp} | ${status.toUpperCase()} | ${info.name} | ${info.id} | ${info.uri}\n`;
|
|
95
|
+
|
|
96
|
+
fs.appendFileSync(this.outputFile, line, 'utf8');
|
|
97
|
+
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Error in handleTestCaseFinished:', error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
handleTestRunFinished() {
|
|
104
|
+
if (this.workerId !== '0') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
this.mergeResults();
|
|
110
|
+
}, 1000);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
mergeResults() {
|
|
114
|
+
try {
|
|
115
|
+
const testStatusFile = path.join(this.outputDir, 'test-status.txt');
|
|
116
|
+
|
|
117
|
+
const files = fs.readdirSync(this.outputDir)
|
|
118
|
+
.filter(f => f.startsWith('test-results-') && f.endsWith('.txt'))
|
|
119
|
+
.sort();
|
|
120
|
+
|
|
121
|
+
if (files.length === 0) {
|
|
122
|
+
console.log('No result files found to merge');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const combined = files
|
|
127
|
+
.map(f => fs.readFileSync(path.join(this.outputDir, f), 'utf8'))
|
|
128
|
+
.join('');
|
|
129
|
+
|
|
130
|
+
fs.writeFileSync(testStatusFile, combined, 'utf8');
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Error merging results:', error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = StatusFormatter;
|