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.
@@ -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
- * ![step-by-step-report](https://codecept.io/img/codeceptjs-slideshow.gif)
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
- &laquo;
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
- `
@@ -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
- }