codeceptjs 4.0.0-rc.16 → 4.0.0-rc.18
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/bin/codecept.js +10 -1
- package/bin/mcp-server.js +541 -172
- package/docs/webapi/seeFileDownloaded.mustache +23 -0
- package/lib/aria.js +260 -0
- package/lib/command/dryRun.js +14 -0
- package/lib/command/list.js +150 -10
- package/lib/config.js +68 -4
- package/lib/container.js +34 -2
- package/lib/helper/Playwright.js +1 -5
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- package/lib/html.js +87 -16
- package/lib/locator.js +12 -1
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +72 -84
- package/lib/plugin/browser.js +76 -0
- package/lib/plugin/heal.js +44 -1
- package/lib/plugin/pageInfo.js +51 -48
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/screencast.js +287 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -170
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +25 -0
- package/package.json +6 -6
- package/typings/index.d.ts +0 -5
- package/lib/helper/AI.js +0 -214
- package/lib/plugin/pauseOn.js +0 -167
- package/lib/plugin/stepByStepReport.js +0 -432
- package/lib/plugin/subtitles.js +0 -89
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
import colors from 'chalk'
|
|
2
|
-
import crypto from 'crypto'
|
|
3
|
-
import figures from 'figures'
|
|
4
|
-
import fs from 'fs'
|
|
5
|
-
import { mkdirp } from 'mkdirp'
|
|
6
|
-
import path from 'path'
|
|
7
|
-
import * as cheerio from 'cheerio'
|
|
8
|
-
|
|
9
|
-
import store from '../store.js'
|
|
10
|
-
import Container from '../container.js'
|
|
11
|
-
import recorder from '../recorder.js'
|
|
12
|
-
import event from '../event.js'
|
|
13
|
-
import output from '../output.js'
|
|
14
|
-
import { template, deleteDir } from '../utils.js'
|
|
15
|
-
|
|
16
|
-
const supportedHelpers = Container.STANDARD_ACTING_HELPERS
|
|
17
|
-
|
|
18
|
-
const defaultConfig = {
|
|
19
|
-
deleteSuccessful: true,
|
|
20
|
-
animateSlides: true,
|
|
21
|
-
ignoreSteps: [],
|
|
22
|
-
fullPageScreenshots: false,
|
|
23
|
-
output: store.outputDir,
|
|
24
|
-
screenshotsForAllureReport: false,
|
|
25
|
-
disableScreenshotOnFail: true,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const templates = {}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 
|
|
32
|
-
*
|
|
33
|
-
* Generates step by step report for a test.
|
|
34
|
-
* After each step in a test a screenshot is created. After test executed screenshots are combined into slideshow.
|
|
35
|
-
* By default, reports are generated only for failed tests.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* Run tests with plugin enabled:
|
|
39
|
-
*
|
|
40
|
-
* ```
|
|
41
|
-
* npx codeceptjs run --plugins stepByStepReport
|
|
42
|
-
* ```
|
|
43
|
-
*
|
|
44
|
-
* #### Configuration
|
|
45
|
-
*
|
|
46
|
-
* ```js
|
|
47
|
-
* "plugins": {
|
|
48
|
-
* "stepByStepReport": {
|
|
49
|
-
* "enabled": true
|
|
50
|
-
* }
|
|
51
|
-
* }
|
|
52
|
-
* ```
|
|
53
|
-
*
|
|
54
|
-
* Possible config options:
|
|
55
|
-
*
|
|
56
|
-
* * `deleteSuccessful`: do not save screenshots for successfully executed tests. Default: true.
|
|
57
|
-
* * `animateSlides`: should animation for slides to be used. Default: true.
|
|
58
|
-
* * `ignoreSteps`: steps to ignore in report. Array of RegExps is expected. Recommended to skip `grab*` and `wait*` steps.
|
|
59
|
-
* * `fullPageScreenshots`: should full page screenshots be used. Default: false.
|
|
60
|
-
* * `output`: a directory where reports should be stored. Default: `output`.
|
|
61
|
-
* * `screenshotsForAllureReport`: If Allure plugin is enabled this plugin attaches each saved screenshot to allure report. Default: false.
|
|
62
|
-
* * `disableScreenshotOnFail : Disables the capturing of screeshots after the failed step. Default: true.
|
|
63
|
-
*
|
|
64
|
-
* @param {*} config
|
|
65
|
-
*/
|
|
66
|
-
|
|
67
|
-
export default function (config) {
|
|
68
|
-
const helpers = Container.helpers()
|
|
69
|
-
let helper
|
|
70
|
-
|
|
71
|
-
config = Object.assign(defaultConfig, config)
|
|
72
|
-
|
|
73
|
-
for (const helperName of supportedHelpers) {
|
|
74
|
-
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
75
|
-
helper = helpers[helperName]
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (!helper) return // no helpers for screenshot
|
|
80
|
-
|
|
81
|
-
let dir
|
|
82
|
-
let stepNum
|
|
83
|
-
let slides = {}
|
|
84
|
-
let error
|
|
85
|
-
let savedStep = null
|
|
86
|
-
let currentTest = null
|
|
87
|
-
let scenarioFailed = false
|
|
88
|
-
|
|
89
|
-
const recordedTests = {}
|
|
90
|
-
const pad = '0000'
|
|
91
|
-
const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output
|
|
92
|
-
|
|
93
|
-
event.dispatcher.on(event.suite.before, suite => {
|
|
94
|
-
stepNum = -1
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
event.dispatcher.on(event.test.before, test => {
|
|
98
|
-
const sha256hash = crypto
|
|
99
|
-
.createHash('sha256')
|
|
100
|
-
.update(test.file + test.title)
|
|
101
|
-
.digest('hex')
|
|
102
|
-
dir = path.join(reportDir, `record_${sha256hash}`)
|
|
103
|
-
mkdirp.sync(dir)
|
|
104
|
-
stepNum = 0
|
|
105
|
-
error = null
|
|
106
|
-
slides = {}
|
|
107
|
-
savedStep = null
|
|
108
|
-
currentTest = test
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
event.dispatcher.on(event.step.failed, step => {
|
|
112
|
-
recorder.add('screenshot of failed test', async () => persistStep(step), true)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
event.dispatcher.on(event.step.after, step => {
|
|
116
|
-
recorder.add('screenshot of step of test', async () => persistStep(step), true)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
event.dispatcher.on(event.test.passed, test => {
|
|
120
|
-
if (!config.deleteSuccessful) return persist(test)
|
|
121
|
-
// cleanup
|
|
122
|
-
deleteDir(dir)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
|
|
126
|
-
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
|
|
127
|
-
// no browser here
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
persist(test)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
event.dispatcher.on(event.all.result, () => {
|
|
135
|
-
if (Object.keys(recordedTests).length === 0 || !Object.keys(slides).length) return
|
|
136
|
-
generateRecordsHtml(recordedTests)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
event.dispatcher.on(event.workers.result, async () => {
|
|
140
|
-
await recorder.add(() => {
|
|
141
|
-
const recordedTests = getRecordFoldersWithDetails(reportDir)
|
|
142
|
-
generateRecordsHtml(recordedTests)
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
function getRecordFoldersWithDetails(dirPath) {
|
|
147
|
-
let results = {}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const items = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
151
|
-
|
|
152
|
-
items.forEach(item => {
|
|
153
|
-
if (item.isDirectory() && item.name.startsWith('record_')) {
|
|
154
|
-
const recordFolderPath = path.join(dirPath, item.name)
|
|
155
|
-
const indexPath = path.join(recordFolderPath, 'index.html')
|
|
156
|
-
|
|
157
|
-
let name = ''
|
|
158
|
-
if (fs.existsSync(indexPath)) {
|
|
159
|
-
try {
|
|
160
|
-
const htmlContent = fs.readFileSync(indexPath, 'utf-8')
|
|
161
|
-
const $ = cheerio.load(htmlContent)
|
|
162
|
-
name = $('.navbar-brand').text().trim()
|
|
163
|
-
} catch (err) {
|
|
164
|
-
console.error(`Error reading index.html in ${recordFolderPath}:`, err.message)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
results[name || 'Unkown'] = `${item.name}/index.html`
|
|
169
|
-
}
|
|
170
|
-
})
|
|
171
|
-
} catch (err) {
|
|
172
|
-
console.error(`Error reading directory ${dirPath}:`, err.message)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return results
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function generateRecordsHtml(recordedTests) {
|
|
179
|
-
let links = ''
|
|
180
|
-
|
|
181
|
-
for (const link in recordedTests) {
|
|
182
|
-
links += `<li><a href="${recordedTests[link]}">${link}</a></li>\n`
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const indexHTML = template(templates.index, {
|
|
186
|
-
time: Date().toString(),
|
|
187
|
-
records: links,
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
fs.writeFileSync(path.join(reportDir, 'records.html'), indexHTML)
|
|
191
|
-
|
|
192
|
-
output.print(`${figures.circleFilled} Step-by-step preview: ${colors.white.bold(`file://${reportDir}/records.html`)}`)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function persistStep(step) {
|
|
196
|
-
if (stepNum === -1) return // Ignore steps from BeforeSuite function
|
|
197
|
-
if (isStepIgnored(step)) return
|
|
198
|
-
if (savedStep === step) return // already saved
|
|
199
|
-
// Ignore steps from BeforeSuite function
|
|
200
|
-
if (scenarioFailed && config.disableScreenshotOnFail) return
|
|
201
|
-
if (step.metaStep && step.metaStep.name === 'BeforeSuite') return
|
|
202
|
-
if (!currentTest) return // Ignore steps from AfterSuite
|
|
203
|
-
|
|
204
|
-
const fileName = `${pad.substring(0, pad.length - stepNum.toString().length) + stepNum.toString()}.png`
|
|
205
|
-
if (step.status === 'failed') {
|
|
206
|
-
scenarioFailed = true
|
|
207
|
-
}
|
|
208
|
-
stepNum++
|
|
209
|
-
slides[fileName] = step
|
|
210
|
-
try {
|
|
211
|
-
const screenshotPath = path.join(dir, fileName)
|
|
212
|
-
await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
|
|
213
|
-
|
|
214
|
-
step.artifacts = step.artifacts || {}
|
|
215
|
-
step.artifacts.screenshot = screenshotPath
|
|
216
|
-
} catch (err) {
|
|
217
|
-
output.plugin(`Can't save step screenshot: ${err}`)
|
|
218
|
-
error = err
|
|
219
|
-
return
|
|
220
|
-
} finally {
|
|
221
|
-
savedStep = step
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (!currentTest.artifacts.screenshots) currentTest.artifacts.screenshots = []
|
|
225
|
-
// added attachments to test
|
|
226
|
-
currentTest.artifacts.screenshots.push(path.join(dir, fileName))
|
|
227
|
-
|
|
228
|
-
const allureReporter = Container.plugins('allure')
|
|
229
|
-
if (allureReporter && config.screenshotsForAllureReport) {
|
|
230
|
-
output.plugin('stepByStepReport', 'Adding screenshot to Allure')
|
|
231
|
-
allureReporter.addAttachment(`Screenshot of step ${step}`, fs.readFileSync(path.join(dir, fileName)), 'image/png')
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function persist(test) {
|
|
236
|
-
if (error) return
|
|
237
|
-
|
|
238
|
-
let indicatorHtml = ''
|
|
239
|
-
let slideHtml = ''
|
|
240
|
-
|
|
241
|
-
for (const i in slides) {
|
|
242
|
-
const step = slides[i]
|
|
243
|
-
const stepNum = parseInt(i, 10)
|
|
244
|
-
indicatorHtml += template(templates.indicator, {
|
|
245
|
-
step: stepNum,
|
|
246
|
-
isActive: stepNum ? '' : 'class="active"',
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
slideHtml += template(templates.slides, {
|
|
250
|
-
image: i,
|
|
251
|
-
caption: step.toString().replace(/\[\d{2}m/g, ''), // remove ANSI escape sequence
|
|
252
|
-
isActive: stepNum ? '' : 'active',
|
|
253
|
-
isError: step.status === 'failed' ? 'error' : '',
|
|
254
|
-
})
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const html = template(templates.global, {
|
|
258
|
-
indicators: indicatorHtml,
|
|
259
|
-
slides: slideHtml,
|
|
260
|
-
feature: test.parent && test.parent.title,
|
|
261
|
-
test: test.title,
|
|
262
|
-
carousel_class: config.animateSlides ? ' slide' : '',
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
const index = path.join(dir, 'index.html')
|
|
266
|
-
fs.writeFileSync(index, html)
|
|
267
|
-
recordedTests[`${test.parent.title}: ${test.title}`] = path.relative(reportDir, index)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function isStepIgnored(step) {
|
|
271
|
-
if (!config.ignoreSteps) return
|
|
272
|
-
for (const pattern of config.ignoreSteps || []) {
|
|
273
|
-
if (step.name.match(pattern)) return true
|
|
274
|
-
}
|
|
275
|
-
return false
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
templates.slides = `
|
|
280
|
-
<div class="item {{isActive}}">
|
|
281
|
-
<div class="fill">
|
|
282
|
-
<img src="{{image}}">
|
|
283
|
-
</div>
|
|
284
|
-
<div class="carousel-caption {{isError}}">
|
|
285
|
-
<h2>{{caption}}</h2>
|
|
286
|
-
<small>scroll up and down to see the full page</small>
|
|
287
|
-
</div>
|
|
288
|
-
</div>
|
|
289
|
-
`
|
|
290
|
-
|
|
291
|
-
templates.indicator = `
|
|
292
|
-
<li data-target="#steps" data-slide-to="{{step}}" {{isActive}}></li>
|
|
293
|
-
`
|
|
294
|
-
|
|
295
|
-
templates.index = `
|
|
296
|
-
<!DOCTYPE html>
|
|
297
|
-
<html lang="en">
|
|
298
|
-
<head>
|
|
299
|
-
<meta charset="utf-8">
|
|
300
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
301
|
-
<title>Step by Steps Report</title>
|
|
302
|
-
|
|
303
|
-
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
|
|
304
|
-
</head>
|
|
305
|
-
<body>
|
|
306
|
-
<!-- Navigation -->
|
|
307
|
-
<nav class="navbar navbar-default" role="navigation">
|
|
308
|
-
<div class="navbar-header">
|
|
309
|
-
<a class="navbar-brand" href="#">Step by Step Report
|
|
310
|
-
</a>
|
|
311
|
-
</div>
|
|
312
|
-
</nav>
|
|
313
|
-
<div class="container">
|
|
314
|
-
<h1>Recorded <small>@ {{time}}</small></h1>
|
|
315
|
-
<ul>
|
|
316
|
-
{{records}}
|
|
317
|
-
</ul>
|
|
318
|
-
</div>
|
|
319
|
-
|
|
320
|
-
</body>
|
|
321
|
-
</html>
|
|
322
|
-
`
|
|
323
|
-
|
|
324
|
-
templates.global = `
|
|
325
|
-
<!DOCTYPE html>
|
|
326
|
-
<html lang="en">
|
|
327
|
-
<head>
|
|
328
|
-
<meta charset="utf-8">
|
|
329
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
330
|
-
<title>Recorder Result</title>
|
|
331
|
-
|
|
332
|
-
<!-- Bootstrap Core CSS -->
|
|
333
|
-
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
|
|
334
|
-
|
|
335
|
-
<style>
|
|
336
|
-
html,
|
|
337
|
-
body {
|
|
338
|
-
height: 100%;
|
|
339
|
-
}
|
|
340
|
-
.carousel,
|
|
341
|
-
.item,
|
|
342
|
-
.active {
|
|
343
|
-
height: 100%;
|
|
344
|
-
}
|
|
345
|
-
.navbar {
|
|
346
|
-
margin-bottom: 0px !important;
|
|
347
|
-
}
|
|
348
|
-
.carousel-caption {
|
|
349
|
-
background: rgba(0,0,0,0.8);
|
|
350
|
-
padding-bottom: 50px !important;
|
|
351
|
-
}
|
|
352
|
-
.carousel-caption.error {
|
|
353
|
-
background: #c0392b !important;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
.carousel-inner {
|
|
357
|
-
height: 100%;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
.fill {
|
|
361
|
-
width: 100%;
|
|
362
|
-
height: 100%;
|
|
363
|
-
text-align: center;
|
|
364
|
-
overflow-y: scroll;
|
|
365
|
-
background-position: top;
|
|
366
|
-
-webkit-background-size: cover;
|
|
367
|
-
-moz-background-size: cover;
|
|
368
|
-
background-size: cover;
|
|
369
|
-
-o-background-size: cover;
|
|
370
|
-
}
|
|
371
|
-
</style>
|
|
372
|
-
</head>
|
|
373
|
-
<body>
|
|
374
|
-
<!-- Navigation -->
|
|
375
|
-
<nav class="navbar navbar-default" role="navigation">
|
|
376
|
-
<div class="navbar-header">
|
|
377
|
-
<a class="navbar-brand" href="../records.html">
|
|
378
|
-
«
|
|
379
|
-
{{feature}}
|
|
380
|
-
<small>{{test}}</small>
|
|
381
|
-
</a>
|
|
382
|
-
</div>
|
|
383
|
-
</nav>
|
|
384
|
-
<header id="steps" class="carousel{{carousel_class}}">
|
|
385
|
-
<!-- Indicators -->
|
|
386
|
-
<ol class="carousel-indicators">
|
|
387
|
-
{{indicators}}
|
|
388
|
-
</ol>
|
|
389
|
-
|
|
390
|
-
<!-- Wrapper for Slides -->
|
|
391
|
-
<div class="carousel-inner">
|
|
392
|
-
{{slides}}
|
|
393
|
-
</div>
|
|
394
|
-
|
|
395
|
-
<!-- Controls -->
|
|
396
|
-
<a class="left carousel-control" href="#steps" data-slide="prev">
|
|
397
|
-
<span class="icon-prev"></span>
|
|
398
|
-
</a>
|
|
399
|
-
<a class="right carousel-control" href="#steps" data-slide="next">
|
|
400
|
-
<span class="icon-next"></span>
|
|
401
|
-
</a>
|
|
402
|
-
|
|
403
|
-
</header>
|
|
404
|
-
|
|
405
|
-
<!-- jQuery -->
|
|
406
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
|
407
|
-
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
|
408
|
-
|
|
409
|
-
<!-- Script to Activate the Carousel -->
|
|
410
|
-
<script>
|
|
411
|
-
$('.carousel').carousel({
|
|
412
|
-
wrap: true,
|
|
413
|
-
interval: false
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
$(document).bind('keyup', function(e) {
|
|
417
|
-
if(e.keyCode==39){
|
|
418
|
-
jQuery('a.carousel-control.right').trigger('click');
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
else if(e.keyCode==37){
|
|
422
|
-
jQuery('a.carousel-control.left').trigger('click');
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
</script>
|
|
428
|
-
|
|
429
|
-
</body>
|
|
430
|
-
|
|
431
|
-
</html>
|
|
432
|
-
`
|
package/lib/plugin/subtitles.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from 'uuid'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
const fsPromise = fs.promises
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import event from '../event.js'
|
|
6
|
-
|
|
7
|
-
// This will convert a given timestamp in milliseconds to
|
|
8
|
-
// an SRT recognized timestamp, ie HH:mm:ss,SSS
|
|
9
|
-
function formatTimestamp(timestampInMs) {
|
|
10
|
-
const date = new Date(0, 0, 0, 0, 0, 0, timestampInMs)
|
|
11
|
-
const hours = date.getHours()
|
|
12
|
-
const minutes = date.getMinutes()
|
|
13
|
-
const seconds = date.getSeconds()
|
|
14
|
-
const ms = timestampInMs - (hours * 3600000 + minutes * 60000 + seconds * 1000)
|
|
15
|
-
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let steps = {}
|
|
19
|
-
let testStartedAt
|
|
20
|
-
/**
|
|
21
|
-
* Automatically captures steps as subtitle, and saves it as an artifact when a video is found for a failed test
|
|
22
|
-
*
|
|
23
|
-
* #### Configuration
|
|
24
|
-
* ```js
|
|
25
|
-
* plugins: {
|
|
26
|
-
* subtitles: {
|
|
27
|
-
* enabled: true
|
|
28
|
-
* }
|
|
29
|
-
* }
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
export default function () {
|
|
33
|
-
event.dispatcher.on(event.test.before, _ => {
|
|
34
|
-
testStartedAt = Date.now()
|
|
35
|
-
steps = {}
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
event.dispatcher.on(event.step.started, step => {
|
|
39
|
-
const stepStartedAt = Date.now()
|
|
40
|
-
step.id = uuidv4()
|
|
41
|
-
|
|
42
|
-
let title = `${step.actor}.${step.name}(${step.args ? step.args.join(',') : ''})`
|
|
43
|
-
if (title.length > 100) {
|
|
44
|
-
title = `${title.substring(0, 100)}...`
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
steps[step.id] = {
|
|
48
|
-
start: formatTimestamp(stepStartedAt - testStartedAt),
|
|
49
|
-
startedAt: stepStartedAt,
|
|
50
|
-
title,
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
event.dispatcher.on(event.step.finished, step => {
|
|
55
|
-
if (step && step.id && steps[step.id]) {
|
|
56
|
-
steps[step.id].end = formatTimestamp(Date.now() - testStartedAt)
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
event.dispatcher.on(event.test.after, async test => {
|
|
61
|
-
if (test && test.artifacts && test.artifacts.video) {
|
|
62
|
-
const stepsSortedByStartTime = Object.values(steps)
|
|
63
|
-
stepsSortedByStartTime.sort((stepA, stepB) => {
|
|
64
|
-
return stepA.startedAt - stepB.startedAt
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
let subtitle = ''
|
|
68
|
-
|
|
69
|
-
// For an SRT file, every subtitle has to be in the format as mentioned below:
|
|
70
|
-
//
|
|
71
|
-
// 1
|
|
72
|
-
// HH:mm:ss,SSS --> HH:mm:ss,SSS
|
|
73
|
-
// [title]
|
|
74
|
-
stepsSortedByStartTime.forEach((step, index) => {
|
|
75
|
-
if (step.end) {
|
|
76
|
-
subtitle = `${subtitle}${index + 1}
|
|
77
|
-
${step.start} --> ${step.end}
|
|
78
|
-
${step.title}
|
|
79
|
-
|
|
80
|
-
`
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
const { dir: artifactsDirectory, name: fileName } = path.parse(test.artifacts.video)
|
|
85
|
-
await fsPromise.writeFile(`${artifactsDirectory}/${fileName}.srt`, subtitle)
|
|
86
|
-
test.artifacts.subtitle = `${artifactsDirectory}/${fileName}.srt`
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
}
|