codeceptjs 4.0.0-beta.5 → 4.0.0-beta.7.esm-aria
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/README.md +0 -45
- package/bin/codecept.js +46 -57
- package/lib/actor.js +15 -11
- package/lib/ai.js +6 -5
- package/lib/assert/empty.js +9 -8
- package/lib/assert/equal.js +15 -17
- package/lib/assert/error.js +2 -2
- package/lib/assert/include.js +9 -11
- package/lib/assert/throws.js +1 -1
- package/lib/assert/truth.js +8 -5
- package/lib/assert.js +18 -18
- package/lib/codecept.js +66 -107
- package/lib/colorUtils.js +48 -50
- package/lib/command/check.js +32 -27
- package/lib/command/configMigrate.js +11 -10
- package/lib/command/definitions.js +16 -10
- package/lib/command/dryRun.js +16 -16
- package/lib/command/generate.js +29 -26
- package/lib/command/gherkin/init.js +36 -38
- package/lib/command/gherkin/snippets.js +14 -14
- package/lib/command/gherkin/steps.js +21 -18
- package/lib/command/info.js +8 -8
- package/lib/command/init.js +34 -31
- package/lib/command/interactive.js +11 -10
- package/lib/command/list.js +10 -9
- package/lib/command/run-multiple/chunk.js +5 -5
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +3 -3
- package/lib/command/run-multiple.js +16 -13
- package/lib/command/run-rerun.js +6 -7
- package/lib/command/run-workers.js +10 -24
- package/lib/command/run.js +8 -8
- package/lib/command/utils.js +20 -18
- package/lib/command/workers/runTests.js +117 -269
- package/lib/config.js +111 -49
- package/lib/container.js +299 -102
- package/lib/data/context.js +6 -5
- package/lib/data/dataScenarioConfig.js +1 -1
- package/lib/data/dataTableArgument.js +1 -1
- package/lib/data/table.js +1 -1
- package/lib/effects.js +94 -10
- package/lib/els.js +11 -9
- package/lib/event.js +11 -10
- package/lib/globals.js +141 -0
- package/lib/heal.js +12 -12
- package/lib/helper/AI.js +1 -1
- package/lib/helper/ApiDataFactory.js +16 -13
- package/lib/helper/FileSystem.js +32 -12
- package/lib/helper/GraphQL.js +1 -1
- package/lib/helper/GraphQLDataFactory.js +1 -1
- package/lib/helper/JSONResponse.js +19 -30
- package/lib/helper/Mochawesome.js +9 -28
- package/lib/helper/Playwright.js +668 -265
- package/lib/helper/Puppeteer.js +284 -169
- package/lib/helper/REST.js +29 -12
- package/lib/helper/WebDriver.js +192 -71
- package/lib/helper/errors/ConnectionRefused.js +6 -6
- package/lib/helper/errors/ElementAssertion.js +11 -16
- package/lib/helper/errors/ElementNotFound.js +5 -9
- package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
- package/lib/helper/extras/Console.js +11 -11
- package/lib/helper/extras/PlaywrightLocator.js +110 -0
- package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
- package/lib/helper/extras/PlaywrightRestartOpts.js +23 -23
- package/lib/helper/extras/Popup.js +1 -1
- package/lib/helper/extras/React.js +29 -30
- package/lib/helper/network/actions.js +33 -48
- package/lib/helper/network/utils.js +76 -83
- package/lib/helper/scripts/blurElement.js +6 -6
- package/lib/helper/scripts/focusElement.js +6 -6
- package/lib/helper/scripts/highlightElement.js +9 -9
- package/lib/helper/scripts/isElementClickable.js +34 -34
- package/lib/helper.js +2 -1
- package/lib/history.js +23 -20
- package/lib/hooks.js +10 -10
- package/lib/html.js +90 -100
- package/lib/index.js +48 -21
- package/lib/listener/config.js +8 -9
- package/lib/listener/emptyRun.js +6 -7
- package/lib/listener/exit.js +4 -3
- package/lib/listener/globalRetry.js +5 -5
- package/lib/listener/globalTimeout.js +11 -10
- package/lib/listener/helpers.js +33 -14
- package/lib/listener/mocha.js +3 -4
- package/lib/listener/result.js +4 -5
- package/lib/listener/steps.js +7 -18
- package/lib/listener/store.js +3 -3
- package/lib/locator.js +213 -192
- package/lib/mocha/asyncWrapper.js +108 -75
- package/lib/mocha/bdd.js +99 -13
- package/lib/mocha/cli.js +60 -27
- package/lib/mocha/factory.js +75 -19
- package/lib/mocha/featureConfig.js +1 -1
- package/lib/mocha/gherkin.js +57 -25
- package/lib/mocha/hooks.js +12 -3
- package/lib/mocha/index.js +13 -4
- package/lib/mocha/inject.js +22 -5
- package/lib/mocha/scenarioConfig.js +2 -2
- package/lib/mocha/suite.js +9 -2
- package/lib/mocha/test.js +10 -13
- package/lib/mocha/ui.js +28 -31
- package/lib/output.js +11 -9
- package/lib/parser.js +44 -44
- package/lib/pause.js +15 -16
- package/lib/plugin/analyze.js +19 -12
- package/lib/plugin/auth.js +20 -21
- package/lib/plugin/autoDelay.js +12 -8
- package/lib/plugin/coverage.js +12 -8
- package/lib/plugin/customLocator.js +3 -3
- package/lib/plugin/customReporter.js +3 -2
- package/lib/plugin/heal.js +14 -9
- package/lib/plugin/pageInfo.js +10 -10
- package/lib/plugin/pauseOnFail.js +4 -3
- package/lib/plugin/retryFailedStep.js +47 -5
- package/lib/plugin/screenshotOnFail.js +75 -37
- package/lib/plugin/stepByStepReport.js +14 -14
- package/lib/plugin/stepTimeout.js +4 -3
- package/lib/plugin/subtitles.js +6 -5
- package/lib/recorder.js +33 -23
- package/lib/rerun.js +69 -26
- package/lib/result.js +4 -4
- package/lib/secret.js +18 -17
- package/lib/session.js +95 -89
- package/lib/step/base.js +6 -6
- package/lib/step/config.js +1 -1
- package/lib/step/func.js +3 -3
- package/lib/step/helper.js +3 -3
- package/lib/step/meta.js +4 -4
- package/lib/step/record.js +11 -11
- package/lib/step/retry.js +3 -3
- package/lib/step/section.js +3 -3
- package/lib/step.js +7 -10
- package/lib/steps.js +9 -5
- package/lib/store.js +1 -1
- package/lib/timeout.js +1 -7
- package/lib/transform.js +8 -8
- package/lib/translation.js +32 -18
- package/lib/utils.js +68 -97
- package/lib/workerStorage.js +16 -17
- package/lib/workers.js +145 -171
- package/package.json +58 -55
- package/translations/de-DE.js +2 -2
- package/translations/fr-FR.js +2 -2
- package/translations/index.js +23 -10
- package/translations/it-IT.js +2 -2
- package/translations/ja-JP.js +2 -2
- package/translations/nl-NL.js +2 -2
- package/translations/pl-PL.js +2 -2
- package/translations/pt-BR.js +2 -2
- package/translations/ru-RU.js +2 -2
- package/translations/utils.js +4 -3
- package/translations/zh-CN.js +2 -2
- package/translations/zh-TW.js +2 -2
- package/typings/index.d.ts +7 -18
- package/typings/promiseBasedTypes.d.ts +3769 -5450
- package/typings/types.d.ts +3953 -5778
- package/bin/test-server.js +0 -53
- package/lib/element/WebElement.js +0 -327
- package/lib/helper/Nightmare.js +0 -1486
- package/lib/helper/Protractor.js +0 -1840
- package/lib/helper/TestCafe.js +0 -1391
- package/lib/helper/clientscripts/nightmare.js +0 -213
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -43
- package/lib/helper/testcafe/testControllerHolder.js +0 -42
- package/lib/helper/testcafe/testcafe-utils.js +0 -61
- package/lib/listener/retryEnhancer.js +0 -85
- package/lib/plugin/allure.js +0 -15
- package/lib/plugin/autoLogin.js +0 -5
- package/lib/plugin/commentStep.js +0 -141
- package/lib/plugin/eachElement.js +0 -127
- package/lib/plugin/fakerTransform.js +0 -49
- package/lib/plugin/htmlReporter.js +0 -1947
- package/lib/plugin/retryTo.js +0 -16
- package/lib/plugin/selenoid.js +0 -364
- package/lib/plugin/standardActingHelpers.js +0 -6
- package/lib/plugin/tryTo.js +0 -16
- package/lib/plugin/wdio.js +0 -247
- package/lib/test-server.js +0 -323
- package/lib/within.js +0 -90
|
@@ -1,1947 +0,0 @@
|
|
|
1
|
-
const fs = require('fs')
|
|
2
|
-
const path = require('path')
|
|
3
|
-
const mkdirp = require('mkdirp')
|
|
4
|
-
const crypto = require('crypto')
|
|
5
|
-
const { template } = require('../utils')
|
|
6
|
-
const { getMachineInfo } = require('../command/info')
|
|
7
|
-
|
|
8
|
-
const event = require('../event')
|
|
9
|
-
const output = require('../output')
|
|
10
|
-
const Codecept = require('../codecept')
|
|
11
|
-
|
|
12
|
-
const defaultConfig = {
|
|
13
|
-
output: global.output_dir || './output',
|
|
14
|
-
reportFileName: 'report.html',
|
|
15
|
-
includeArtifacts: true,
|
|
16
|
-
showSteps: true,
|
|
17
|
-
showSkipped: true,
|
|
18
|
-
showMetadata: true,
|
|
19
|
-
showTags: true,
|
|
20
|
-
showRetries: true,
|
|
21
|
-
exportStats: false,
|
|
22
|
-
exportStatsPath: './stats.json',
|
|
23
|
-
keepHistory: false,
|
|
24
|
-
historyPath: './test-history.json',
|
|
25
|
-
maxHistoryEntries: 50,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* HTML Reporter Plugin for CodeceptJS
|
|
30
|
-
*
|
|
31
|
-
* Generates comprehensive HTML reports showing:
|
|
32
|
-
* - Test statistics
|
|
33
|
-
* - Feature/Scenario details
|
|
34
|
-
* - Individual step results
|
|
35
|
-
* - Test artifacts (screenshots, etc.)
|
|
36
|
-
*
|
|
37
|
-
* ## Configuration
|
|
38
|
-
*
|
|
39
|
-
* ```js
|
|
40
|
-
* "plugins": {
|
|
41
|
-
* "htmlReporter": {
|
|
42
|
-
* "enabled": true,
|
|
43
|
-
* "output": "./output",
|
|
44
|
-
* "reportFileName": "report.html",
|
|
45
|
-
* "includeArtifacts": true,
|
|
46
|
-
* "showSteps": true,
|
|
47
|
-
* "showSkipped": true,
|
|
48
|
-
* "showMetadata": true,
|
|
49
|
-
* "showTags": true,
|
|
50
|
-
* "showRetries": true,
|
|
51
|
-
* "exportStats": false,
|
|
52
|
-
* "exportStatsPath": "./stats.json",
|
|
53
|
-
* "keepHistory": false,
|
|
54
|
-
* "historyPath": "./test-history.json",
|
|
55
|
-
* "maxHistoryEntries": 50
|
|
56
|
-
* }
|
|
57
|
-
* }
|
|
58
|
-
* ```
|
|
59
|
-
*/
|
|
60
|
-
module.exports = function (config) {
|
|
61
|
-
const options = { ...defaultConfig, ...config }
|
|
62
|
-
let reportData = {
|
|
63
|
-
stats: {},
|
|
64
|
-
tests: [],
|
|
65
|
-
failures: [],
|
|
66
|
-
hooks: [],
|
|
67
|
-
startTime: null,
|
|
68
|
-
endTime: null,
|
|
69
|
-
retries: [],
|
|
70
|
-
config: options,
|
|
71
|
-
}
|
|
72
|
-
let currentTestSteps = []
|
|
73
|
-
let currentTestHooks = []
|
|
74
|
-
let currentBddSteps = [] // Track BDD/Gherkin steps
|
|
75
|
-
let testRetryAttempts = new Map() // Track retry attempts per test
|
|
76
|
-
let currentSuite = null // Track current suite for BDD detection
|
|
77
|
-
|
|
78
|
-
// Initialize report directory
|
|
79
|
-
const reportDir = options.output ? path.resolve(global.codecept_dir, options.output) : path.resolve(global.output_dir || './output')
|
|
80
|
-
mkdirp.sync(reportDir)
|
|
81
|
-
|
|
82
|
-
// Track overall test execution
|
|
83
|
-
event.dispatcher.on(event.all.before, () => {
|
|
84
|
-
reportData.startTime = new Date()
|
|
85
|
-
output.plugin('htmlReporter', 'Starting HTML report generation...')
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
// Track test start to initialize steps and hooks collection
|
|
89
|
-
event.dispatcher.on(event.test.before, test => {
|
|
90
|
-
currentTestSteps = []
|
|
91
|
-
currentTestHooks = []
|
|
92
|
-
currentBddSteps = []
|
|
93
|
-
|
|
94
|
-
// Track current suite for BDD detection
|
|
95
|
-
currentSuite = test.parent
|
|
96
|
-
|
|
97
|
-
// Track retry attempts
|
|
98
|
-
if (test.retriedTest && test.retriedTest()) {
|
|
99
|
-
const originalTest = test.retriedTest()
|
|
100
|
-
const testId = generateTestId(originalTest)
|
|
101
|
-
if (!testRetryAttempts.has(testId)) {
|
|
102
|
-
testRetryAttempts.set(testId, 0)
|
|
103
|
-
}
|
|
104
|
-
testRetryAttempts.set(testId, testRetryAttempts.get(testId) + 1)
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
// Collect step information
|
|
109
|
-
event.dispatcher.on(event.step.started, step => {
|
|
110
|
-
step.htmlReporterStartTime = Date.now()
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
event.dispatcher.on(event.step.finished, step => {
|
|
114
|
-
if (step.htmlReporterStartTime) {
|
|
115
|
-
step.duration = Date.now() - step.htmlReporterStartTime
|
|
116
|
-
}
|
|
117
|
-
currentTestSteps.push({
|
|
118
|
-
name: step.name,
|
|
119
|
-
actor: step.actor,
|
|
120
|
-
args: step.args || [],
|
|
121
|
-
status: step.failed ? 'failed' : 'success',
|
|
122
|
-
duration: step.duration || 0,
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
// Collect hook information
|
|
127
|
-
event.dispatcher.on(event.hook.started, hook => {
|
|
128
|
-
hook.htmlReporterStartTime = Date.now()
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
event.dispatcher.on(event.hook.finished, hook => {
|
|
132
|
-
if (hook.htmlReporterStartTime) {
|
|
133
|
-
hook.duration = Date.now() - hook.htmlReporterStartTime
|
|
134
|
-
}
|
|
135
|
-
const hookInfo = {
|
|
136
|
-
title: hook.title,
|
|
137
|
-
type: hook.type || 'unknown', // before, after, beforeSuite, afterSuite
|
|
138
|
-
status: hook.err ? 'failed' : 'passed',
|
|
139
|
-
duration: hook.duration || 0,
|
|
140
|
-
error: hook.err ? hook.err.message : null,
|
|
141
|
-
}
|
|
142
|
-
currentTestHooks.push(hookInfo)
|
|
143
|
-
reportData.hooks.push(hookInfo)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
// Collect BDD/Gherkin step information
|
|
147
|
-
event.dispatcher.on(event.bddStep.started, step => {
|
|
148
|
-
step.htmlReporterStartTime = Date.now()
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
event.dispatcher.on(event.bddStep.finished, step => {
|
|
152
|
-
if (step.htmlReporterStartTime) {
|
|
153
|
-
step.duration = Date.now() - step.htmlReporterStartTime
|
|
154
|
-
}
|
|
155
|
-
currentBddSteps.push({
|
|
156
|
-
keyword: step.actor || 'Given',
|
|
157
|
-
text: step.name,
|
|
158
|
-
status: step.failed ? 'failed' : 'success',
|
|
159
|
-
duration: step.duration || 0,
|
|
160
|
-
comment: step.comment,
|
|
161
|
-
})
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
// Collect test results
|
|
165
|
-
event.dispatcher.on(event.test.finished, test => {
|
|
166
|
-
const testId = generateTestId(test)
|
|
167
|
-
const retryAttempts = testRetryAttempts.get(testId) || 0
|
|
168
|
-
|
|
169
|
-
// Detect if this is a BDD/Gherkin test
|
|
170
|
-
const isBddTest = isBddGherkinTest(test, currentSuite)
|
|
171
|
-
const steps = isBddTest ? currentBddSteps : currentTestSteps
|
|
172
|
-
const featureInfo = isBddTest ? getBddFeatureInfo(test, currentSuite) : null
|
|
173
|
-
|
|
174
|
-
reportData.tests.push({
|
|
175
|
-
...test,
|
|
176
|
-
id: testId,
|
|
177
|
-
duration: test.duration || 0,
|
|
178
|
-
steps: [...steps], // Copy the steps (BDD or regular)
|
|
179
|
-
hooks: [...currentTestHooks], // Copy the hooks
|
|
180
|
-
artifacts: test.artifacts || [],
|
|
181
|
-
tags: test.tags || [],
|
|
182
|
-
meta: test.meta || {},
|
|
183
|
-
opts: test.opts || {},
|
|
184
|
-
notes: test.notes || [],
|
|
185
|
-
retryAttempts: retryAttempts,
|
|
186
|
-
uid: test.uid,
|
|
187
|
-
isBdd: isBddTest,
|
|
188
|
-
feature: featureInfo,
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
// If this was a retry, track the retry information
|
|
192
|
-
if (retryAttempts > 0) {
|
|
193
|
-
reportData.retries.push({
|
|
194
|
-
testId: testId,
|
|
195
|
-
testTitle: test.title,
|
|
196
|
-
attempts: retryAttempts,
|
|
197
|
-
finalState: test.state,
|
|
198
|
-
duration: test.duration || 0,
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
// Generate final report
|
|
204
|
-
event.dispatcher.on(event.all.result, result => {
|
|
205
|
-
reportData.endTime = new Date()
|
|
206
|
-
reportData.stats = result.stats
|
|
207
|
-
reportData.failures = result.failures || []
|
|
208
|
-
reportData.duration = reportData.endTime - reportData.startTime
|
|
209
|
-
|
|
210
|
-
generateHtmlReport(reportData, options)
|
|
211
|
-
|
|
212
|
-
// Export stats if configured
|
|
213
|
-
if (options.exportStats) {
|
|
214
|
-
exportTestStats(reportData, options)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Save history if configured
|
|
218
|
-
if (options.keepHistory) {
|
|
219
|
-
saveTestHistory(reportData, options)
|
|
220
|
-
}
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
function generateTestId(test) {
|
|
224
|
-
return crypto
|
|
225
|
-
.createHash('sha256')
|
|
226
|
-
.update(`${test.parent?.title || 'unknown'}_${test.title}`)
|
|
227
|
-
.digest('hex')
|
|
228
|
-
.substring(0, 8)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function isBddGherkinTest(test, suite) {
|
|
232
|
-
// Check if the suite has BDD/Gherkin properties
|
|
233
|
-
return !!(suite && (suite.feature || suite.file?.endsWith('.feature')))
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function getBddFeatureInfo(test, suite) {
|
|
237
|
-
if (!suite) return null
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
name: suite.feature?.name || suite.title,
|
|
241
|
-
description: suite.feature?.description || suite.comment || '',
|
|
242
|
-
language: suite.feature?.language || 'en',
|
|
243
|
-
tags: suite.tags || [],
|
|
244
|
-
file: suite.file || '',
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function exportTestStats(data, config) {
|
|
249
|
-
const statsPath = path.resolve(reportDir, config.exportStatsPath)
|
|
250
|
-
|
|
251
|
-
const exportData = {
|
|
252
|
-
timestamp: data.endTime.toISOString(),
|
|
253
|
-
duration: data.duration,
|
|
254
|
-
stats: data.stats,
|
|
255
|
-
retries: data.retries,
|
|
256
|
-
testCount: data.tests.length,
|
|
257
|
-
passedTests: data.tests.filter(t => t.state === 'passed').length,
|
|
258
|
-
failedTests: data.tests.filter(t => t.state === 'failed').length,
|
|
259
|
-
pendingTests: data.tests.filter(t => t.state === 'pending').length,
|
|
260
|
-
tests: data.tests.map(test => ({
|
|
261
|
-
id: test.id,
|
|
262
|
-
title: test.title,
|
|
263
|
-
feature: test.parent?.title || 'Unknown',
|
|
264
|
-
state: test.state,
|
|
265
|
-
duration: test.duration,
|
|
266
|
-
tags: test.tags,
|
|
267
|
-
meta: test.meta,
|
|
268
|
-
retryAttempts: test.retryAttempts,
|
|
269
|
-
uid: test.uid,
|
|
270
|
-
})),
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
fs.writeFileSync(statsPath, JSON.stringify(exportData, null, 2))
|
|
275
|
-
output.print(`Test stats exported to: ${statsPath}`)
|
|
276
|
-
} catch (error) {
|
|
277
|
-
output.print(`Failed to export test stats: ${error.message}`)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function saveTestHistory(data, config) {
|
|
282
|
-
const historyPath = path.resolve(reportDir, config.historyPath)
|
|
283
|
-
let history = []
|
|
284
|
-
|
|
285
|
-
// Load existing history
|
|
286
|
-
try {
|
|
287
|
-
if (fs.existsSync(historyPath)) {
|
|
288
|
-
history = JSON.parse(fs.readFileSync(historyPath, 'utf8'))
|
|
289
|
-
}
|
|
290
|
-
} catch (error) {
|
|
291
|
-
output.print(`Failed to load existing history: ${error.message}`)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Add current run to history
|
|
295
|
-
history.unshift({
|
|
296
|
-
timestamp: data.endTime.toISOString(),
|
|
297
|
-
duration: data.duration,
|
|
298
|
-
stats: data.stats,
|
|
299
|
-
retries: data.retries.length,
|
|
300
|
-
testCount: data.tests.length,
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
// Limit history entries
|
|
304
|
-
if (history.length > config.maxHistoryEntries) {
|
|
305
|
-
history = history.slice(0, config.maxHistoryEntries)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2))
|
|
310
|
-
output.print(`Test history saved to: ${historyPath}`)
|
|
311
|
-
} catch (error) {
|
|
312
|
-
output.print(`Failed to save test history: ${error.message}`)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async function generateHtmlReport(data, config) {
|
|
317
|
-
const reportPath = path.join(reportDir, config.reportFileName)
|
|
318
|
-
|
|
319
|
-
// Load history if available
|
|
320
|
-
let history = []
|
|
321
|
-
if (config.keepHistory) {
|
|
322
|
-
const historyPath = path.resolve(reportDir, config.historyPath)
|
|
323
|
-
try {
|
|
324
|
-
if (fs.existsSync(historyPath)) {
|
|
325
|
-
history = JSON.parse(fs.readFileSync(historyPath, 'utf8')).slice(0, 10) // Last 10 runs for chart
|
|
326
|
-
}
|
|
327
|
-
} catch (error) {
|
|
328
|
-
output.print(`Failed to load history for report: ${error.message}`)
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Get system information
|
|
333
|
-
const systemInfo = await getMachineInfo()
|
|
334
|
-
|
|
335
|
-
const html = template(getHtmlTemplate(), {
|
|
336
|
-
title: `CodeceptJS Test Report v${Codecept.version()}`,
|
|
337
|
-
timestamp: data.endTime.toISOString(),
|
|
338
|
-
duration: formatDuration(data.duration),
|
|
339
|
-
stats: JSON.stringify(data.stats),
|
|
340
|
-
history: JSON.stringify(history),
|
|
341
|
-
statsHtml: generateStatsHtml(data.stats),
|
|
342
|
-
testsHtml: generateTestsHtml(data.tests, config),
|
|
343
|
-
failuresHtml: generateFailuresHtml(data.failures),
|
|
344
|
-
retriesHtml: config.showRetries ? generateRetriesHtml(data.retries) : '',
|
|
345
|
-
cssStyles: getCssStyles(),
|
|
346
|
-
jsScripts: getJsScripts(),
|
|
347
|
-
showRetries: config.showRetries ? 'block' : 'none',
|
|
348
|
-
showHistory: config.keepHistory && history.length > 0 ? 'block' : 'none',
|
|
349
|
-
failuresDisplay: data.failures && data.failures.length > 0 ? 'block' : 'none',
|
|
350
|
-
codeceptVersion: Codecept.version(),
|
|
351
|
-
systemInfoHtml: generateSystemInfoHtml(systemInfo),
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
fs.writeFileSync(reportPath, html)
|
|
355
|
-
output.print(`HTML Report saved to: ${reportPath}`)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function generateStatsHtml(stats) {
|
|
359
|
-
const passed = stats.passes || 0
|
|
360
|
-
const failed = stats.failures || 0
|
|
361
|
-
const pending = stats.pending || 0
|
|
362
|
-
const total = stats.tests || 0
|
|
363
|
-
|
|
364
|
-
return `
|
|
365
|
-
<div class="stats-cards">
|
|
366
|
-
<div class="stat-card total">
|
|
367
|
-
<h3>Total</h3>
|
|
368
|
-
<span class="stat-number">${total}</span>
|
|
369
|
-
</div>
|
|
370
|
-
<div class="stat-card passed">
|
|
371
|
-
<h3>Passed</h3>
|
|
372
|
-
<span class="stat-number">${passed}</span>
|
|
373
|
-
</div>
|
|
374
|
-
<div class="stat-card failed">
|
|
375
|
-
<h3>Failed</h3>
|
|
376
|
-
<span class="stat-number">${failed}</span>
|
|
377
|
-
</div>
|
|
378
|
-
<div class="stat-card pending">
|
|
379
|
-
<h3>Pending</h3>
|
|
380
|
-
<span class="stat-number">${pending}</span>
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
<div class="pie-chart-container">
|
|
384
|
-
<canvas id="statsChart" width="300" height="300"></canvas>
|
|
385
|
-
<script>
|
|
386
|
-
// Pie chart data will be rendered by JavaScript
|
|
387
|
-
window.chartData = {
|
|
388
|
-
passed: ${passed},
|
|
389
|
-
failed: ${failed},
|
|
390
|
-
pending: ${pending}
|
|
391
|
-
};
|
|
392
|
-
</script>
|
|
393
|
-
</div>
|
|
394
|
-
`
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function generateTestsHtml(tests, config) {
|
|
398
|
-
if (!tests || tests.length === 0) {
|
|
399
|
-
return '<p>No tests found.</p>'
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return tests
|
|
403
|
-
.map(test => {
|
|
404
|
-
const statusClass = test.state || 'unknown'
|
|
405
|
-
const feature = test.isBdd && test.feature ? test.feature.name : test.parent?.title || 'Unknown Feature'
|
|
406
|
-
const steps = config.showSteps && test.steps ? (test.isBdd ? generateBddStepsHtml(test.steps) : generateStepsHtml(test.steps)) : ''
|
|
407
|
-
const featureDetails = test.isBdd && test.feature ? generateBddFeatureHtml(test.feature) : ''
|
|
408
|
-
const hooks = test.hooks && test.hooks.length > 0 ? generateHooksHtml(test.hooks) : ''
|
|
409
|
-
const artifacts = config.includeArtifacts && test.artifacts ? generateArtifactsHtml(test.artifacts) : ''
|
|
410
|
-
const metadata = config.showMetadata && (test.meta || test.opts) ? generateMetadataHtml(test.meta, test.opts) : ''
|
|
411
|
-
const tags = config.showTags && test.tags && test.tags.length > 0 ? generateTagsHtml(test.tags) : ''
|
|
412
|
-
const retries = config.showRetries && test.retryAttempts > 0 ? generateTestRetryHtml(test.retryAttempts) : ''
|
|
413
|
-
const notes = test.notes && test.notes.length > 0 ? generateNotesHtml(test.notes) : ''
|
|
414
|
-
|
|
415
|
-
return `
|
|
416
|
-
<div class="test-item ${statusClass}${test.isBdd ? ' bdd-test' : ''}" id="test-${test.id}" data-feature="${escapeHtml(feature)}" data-status="${statusClass}" data-tags="${(test.tags || []).join(',')}" data-retries="${test.retryAttempts || 0}" data-type="${test.isBdd ? 'bdd' : 'regular'}">
|
|
417
|
-
<div class="test-header" onclick="toggleTestDetails('test-${test.id}')">
|
|
418
|
-
<span class="test-status ${statusClass}">●</span>
|
|
419
|
-
<div class="test-info">
|
|
420
|
-
<h3 class="test-title">${test.isBdd ? `Scenario: ${test.title}` : test.title}</h3>
|
|
421
|
-
<div class="test-meta-line">
|
|
422
|
-
<span class="test-feature">${test.isBdd ? 'Feature: ' : ''}${feature}</span>
|
|
423
|
-
${test.uid ? `<span class="test-uid">${test.uid}</span>` : ''}
|
|
424
|
-
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
425
|
-
${test.retryAttempts > 0 ? `<span class="retry-badge">${test.retryAttempts} retries</span>` : ''}
|
|
426
|
-
${test.isBdd ? '<span class="bdd-badge">Gherkin</span>' : ''}
|
|
427
|
-
</div>
|
|
428
|
-
</div>
|
|
429
|
-
</div>
|
|
430
|
-
<div class="test-details" id="details-test-${test.id}">
|
|
431
|
-
${test.err ? `<div class="error-message"><pre>${escapeHtml(test.err.message || '').replace(/\x1b\[[0-9;]*m/g, '')}</pre></div>` : ''}
|
|
432
|
-
${featureDetails}
|
|
433
|
-
${tags}
|
|
434
|
-
${metadata}
|
|
435
|
-
${retries}
|
|
436
|
-
${notes}
|
|
437
|
-
${hooks}
|
|
438
|
-
${steps}
|
|
439
|
-
${artifacts}
|
|
440
|
-
</div>
|
|
441
|
-
</div>
|
|
442
|
-
`
|
|
443
|
-
})
|
|
444
|
-
.join('')
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function generateStepsHtml(steps) {
|
|
448
|
-
if (!steps || steps.length === 0) return ''
|
|
449
|
-
|
|
450
|
-
const stepsHtml = steps
|
|
451
|
-
.map(step => {
|
|
452
|
-
const statusClass = step.status || 'unknown'
|
|
453
|
-
const args = step.args ? step.args.map(arg => JSON.stringify(arg)).join(', ') : ''
|
|
454
|
-
const stepName = step.name || 'unknown step'
|
|
455
|
-
const actor = step.actor || 'I'
|
|
456
|
-
|
|
457
|
-
return `
|
|
458
|
-
<div class="step-item ${statusClass}">
|
|
459
|
-
<span class="step-status ${statusClass}">●</span>
|
|
460
|
-
<span class="step-title">${actor}.${stepName}(${args})</span>
|
|
461
|
-
<span class="step-duration">${formatDuration(step.duration)}</span>
|
|
462
|
-
</div>
|
|
463
|
-
`
|
|
464
|
-
})
|
|
465
|
-
.join('')
|
|
466
|
-
|
|
467
|
-
return `
|
|
468
|
-
<div class="steps-section">
|
|
469
|
-
<h4>Steps:</h4>
|
|
470
|
-
<div class="steps-list">${stepsHtml}</div>
|
|
471
|
-
</div>
|
|
472
|
-
`
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function generateBddStepsHtml(steps) {
|
|
476
|
-
if (!steps || steps.length === 0) return ''
|
|
477
|
-
|
|
478
|
-
const stepsHtml = steps
|
|
479
|
-
.map(step => {
|
|
480
|
-
const statusClass = step.status || 'unknown'
|
|
481
|
-
const keyword = step.keyword || 'Given'
|
|
482
|
-
const text = step.text || ''
|
|
483
|
-
const comment = step.comment ? `<div class="step-comment">${escapeHtml(step.comment)}</div>` : ''
|
|
484
|
-
|
|
485
|
-
return `
|
|
486
|
-
<div class="bdd-step-item ${statusClass}">
|
|
487
|
-
<span class="step-status ${statusClass}">●</span>
|
|
488
|
-
<span class="bdd-keyword">${keyword}</span>
|
|
489
|
-
<span class="bdd-step-text">${escapeHtml(text)}</span>
|
|
490
|
-
<span class="step-duration">${formatDuration(step.duration)}</span>
|
|
491
|
-
${comment}
|
|
492
|
-
</div>
|
|
493
|
-
`
|
|
494
|
-
})
|
|
495
|
-
.join('')
|
|
496
|
-
|
|
497
|
-
return `
|
|
498
|
-
<div class="bdd-steps-section">
|
|
499
|
-
<h4>Scenario Steps:</h4>
|
|
500
|
-
<div class="bdd-steps-list">${stepsHtml}</div>
|
|
501
|
-
</div>
|
|
502
|
-
`
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function generateBddFeatureHtml(feature) {
|
|
506
|
-
if (!feature) return ''
|
|
507
|
-
|
|
508
|
-
const description = feature.description ? `<div class="feature-description">${escapeHtml(feature.description)}</div>` : ''
|
|
509
|
-
const featureTags = feature.tags && feature.tags.length > 0 ? `<div class="feature-tags">${feature.tags.map(tag => `<span class="feature-tag">${escapeHtml(tag)}</span>`).join('')}</div>` : ''
|
|
510
|
-
|
|
511
|
-
return `
|
|
512
|
-
<div class="bdd-feature-section">
|
|
513
|
-
<h4>Feature Information:</h4>
|
|
514
|
-
<div class="feature-info">
|
|
515
|
-
<div class="feature-name">Feature: ${escapeHtml(feature.name)}</div>
|
|
516
|
-
${description}
|
|
517
|
-
${featureTags}
|
|
518
|
-
${feature.file ? `<div class="feature-file">File: ${escapeHtml(feature.file)}</div>` : ''}
|
|
519
|
-
</div>
|
|
520
|
-
</div>
|
|
521
|
-
`
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function generateHooksHtml(hooks) {
|
|
525
|
-
if (!hooks || hooks.length === 0) return ''
|
|
526
|
-
|
|
527
|
-
const hooksHtml = hooks
|
|
528
|
-
.map(hook => {
|
|
529
|
-
const statusClass = hook.status || 'unknown'
|
|
530
|
-
const hookType = hook.type || 'hook'
|
|
531
|
-
const hookTitle = hook.title || `${hookType} hook`
|
|
532
|
-
|
|
533
|
-
return `
|
|
534
|
-
<div class="hook-item ${statusClass}">
|
|
535
|
-
<span class="hook-status ${statusClass}">●</span>
|
|
536
|
-
<span class="hook-title">${hookType}: ${hookTitle}</span>
|
|
537
|
-
<span class="hook-duration">${formatDuration(hook.duration)}</span>
|
|
538
|
-
${hook.error ? `<div class="hook-error">${escapeHtml(hook.error)}</div>` : ''}
|
|
539
|
-
</div>
|
|
540
|
-
`
|
|
541
|
-
})
|
|
542
|
-
.join('')
|
|
543
|
-
|
|
544
|
-
return `
|
|
545
|
-
<div class="hooks-section">
|
|
546
|
-
<h4>Hooks:</h4>
|
|
547
|
-
<div class="hooks-list">${hooksHtml}</div>
|
|
548
|
-
</div>
|
|
549
|
-
`
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function generateMetadataHtml(meta, opts) {
|
|
553
|
-
const allMeta = { ...(opts || {}), ...(meta || {}) }
|
|
554
|
-
if (!allMeta || Object.keys(allMeta).length === 0) return ''
|
|
555
|
-
|
|
556
|
-
const metaHtml = Object.entries(allMeta)
|
|
557
|
-
.filter(([key, value]) => value !== undefined && value !== null)
|
|
558
|
-
.map(([key, value]) => {
|
|
559
|
-
const displayValue = typeof value === 'object' ? JSON.stringify(value) : value.toString()
|
|
560
|
-
return `<div class="meta-item"><span class="meta-key">${escapeHtml(key)}:</span> <span class="meta-value">${escapeHtml(displayValue)}</span></div>`
|
|
561
|
-
})
|
|
562
|
-
.join('')
|
|
563
|
-
|
|
564
|
-
return `
|
|
565
|
-
<div class="metadata-section">
|
|
566
|
-
<h4>Metadata:</h4>
|
|
567
|
-
<div class="metadata-list">${metaHtml}</div>
|
|
568
|
-
</div>
|
|
569
|
-
`
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function generateTagsHtml(tags) {
|
|
573
|
-
if (!tags || tags.length === 0) return ''
|
|
574
|
-
|
|
575
|
-
const tagsHtml = tags.map(tag => `<span class="test-tag">${escapeHtml(tag)}</span>`).join('')
|
|
576
|
-
|
|
577
|
-
return `
|
|
578
|
-
<div class="tags-section">
|
|
579
|
-
<h4>Tags:</h4>
|
|
580
|
-
<div class="tags-list">${tagsHtml}</div>
|
|
581
|
-
</div>
|
|
582
|
-
`
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function generateNotesHtml(notes) {
|
|
586
|
-
if (!notes || notes.length === 0) return ''
|
|
587
|
-
|
|
588
|
-
const notesHtml = notes.map(note => `<div class="note-item note-${note.type || 'info'}"><span class="note-type">${note.type || 'info'}:</span> <span class="note-text">${escapeHtml(note.text)}</span></div>`).join('')
|
|
589
|
-
|
|
590
|
-
return `
|
|
591
|
-
<div class="notes-section">
|
|
592
|
-
<h4>Notes:</h4>
|
|
593
|
-
<div class="notes-list">${notesHtml}</div>
|
|
594
|
-
</div>
|
|
595
|
-
`
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function generateTestRetryHtml(retryAttempts) {
|
|
599
|
-
return `
|
|
600
|
-
<div class="retry-section">
|
|
601
|
-
<h4>Retry Information:</h4>
|
|
602
|
-
<div class="retry-info">
|
|
603
|
-
<span class="retry-count">Total retry attempts: <strong>${retryAttempts}</strong></span>
|
|
604
|
-
</div>
|
|
605
|
-
</div>
|
|
606
|
-
`
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function generateArtifactsHtml(artifacts) {
|
|
610
|
-
if (!artifacts || artifacts.length === 0) return ''
|
|
611
|
-
|
|
612
|
-
const artifactsHtml = artifacts
|
|
613
|
-
.map(artifact => {
|
|
614
|
-
if (typeof artifact === 'string' && artifact.match(/\.(png|jpg|jpeg|gif)$/i)) {
|
|
615
|
-
const relativePath = path.relative(reportDir, artifact)
|
|
616
|
-
return `<img src="${relativePath}" alt="Screenshot" class="artifact-image" onclick="openImageModal(this.src)"/>`
|
|
617
|
-
}
|
|
618
|
-
return `<div class="artifact-item">${escapeHtml(artifact.toString())}</div>`
|
|
619
|
-
})
|
|
620
|
-
.join('')
|
|
621
|
-
|
|
622
|
-
return `
|
|
623
|
-
<div class="artifacts-section">
|
|
624
|
-
<h4>Artifacts:</h4>
|
|
625
|
-
<div class="artifacts-list">${artifactsHtml}</div>
|
|
626
|
-
</div>
|
|
627
|
-
`
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function generateFailuresHtml(failures) {
|
|
631
|
-
if (!failures || failures.length === 0) {
|
|
632
|
-
return '<p>No failures.</p>'
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return failures
|
|
636
|
-
.map((failure, index) => {
|
|
637
|
-
const failureText = failure.toString().replace(/\x1b\[[0-9;]*m/g, '') // Remove ANSI escape codes
|
|
638
|
-
return `
|
|
639
|
-
<div class="failure-item">
|
|
640
|
-
<h4>Failure ${index + 1}</h4>
|
|
641
|
-
<pre class="failure-details">${escapeHtml(failureText)}</pre>
|
|
642
|
-
</div>
|
|
643
|
-
`
|
|
644
|
-
})
|
|
645
|
-
.join('')
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function generateRetriesHtml(retries) {
|
|
649
|
-
if (!retries || retries.length === 0) {
|
|
650
|
-
return '<p>No retried tests.</p>'
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return retries
|
|
654
|
-
.map(
|
|
655
|
-
retry => `
|
|
656
|
-
<div class="retry-item">
|
|
657
|
-
<h4>${retry.testTitle}</h4>
|
|
658
|
-
<div class="retry-details">
|
|
659
|
-
<span>Attempts: <strong>${retry.attempts}</strong></span>
|
|
660
|
-
<span>Final State: <span class="status-badge ${retry.finalState}">${retry.finalState}</span></span>
|
|
661
|
-
<span>Duration: ${formatDuration(retry.duration)}</span>
|
|
662
|
-
</div>
|
|
663
|
-
</div>
|
|
664
|
-
`,
|
|
665
|
-
)
|
|
666
|
-
.join('')
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function formatDuration(duration) {
|
|
670
|
-
if (!duration) return '0ms'
|
|
671
|
-
if (duration < 1000) return `${duration}ms`
|
|
672
|
-
return `${(duration / 1000).toFixed(2)}s`
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function escapeHtml(unsafe) {
|
|
676
|
-
return unsafe.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function generateSystemInfoHtml(systemInfo) {
|
|
680
|
-
if (!systemInfo) return ''
|
|
681
|
-
|
|
682
|
-
const formatInfo = (key, value) => {
|
|
683
|
-
if (Array.isArray(value) && value.length > 1) {
|
|
684
|
-
return `<div class="info-item"><span class="info-key">${key}:</span> <span class="info-value">${escapeHtml(value[1])}</span></div>`
|
|
685
|
-
} else if (typeof value === 'string' && value !== 'N/A' && value !== 'undefined') {
|
|
686
|
-
return `<div class="info-item"><span class="info-key">${key}:</span> <span class="info-value">${escapeHtml(value)}</span></div>`
|
|
687
|
-
}
|
|
688
|
-
return ''
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const infoItems = [
|
|
692
|
-
formatInfo('Node.js', systemInfo.nodeInfo),
|
|
693
|
-
formatInfo('OS', systemInfo.osInfo),
|
|
694
|
-
formatInfo('CPU', systemInfo.cpuInfo),
|
|
695
|
-
formatInfo('Chrome', systemInfo.chromeInfo),
|
|
696
|
-
formatInfo('Edge', systemInfo.edgeInfo),
|
|
697
|
-
formatInfo('Firefox', systemInfo.firefoxInfo),
|
|
698
|
-
formatInfo('Safari', systemInfo.safariInfo),
|
|
699
|
-
formatInfo('Playwright Browsers', systemInfo.playwrightBrowsers),
|
|
700
|
-
]
|
|
701
|
-
.filter(item => item)
|
|
702
|
-
.join('')
|
|
703
|
-
|
|
704
|
-
if (!infoItems) return ''
|
|
705
|
-
|
|
706
|
-
return `
|
|
707
|
-
<section class="system-info-section">
|
|
708
|
-
<div class="system-info-header" onclick="toggleSystemInfo()">
|
|
709
|
-
<h3>Environment Information</h3>
|
|
710
|
-
<span class="toggle-icon">▼</span>
|
|
711
|
-
</div>
|
|
712
|
-
<div class="system-info-content" id="systemInfoContent">
|
|
713
|
-
<div class="system-info-grid">
|
|
714
|
-
${infoItems}
|
|
715
|
-
</div>
|
|
716
|
-
</div>
|
|
717
|
-
</section>
|
|
718
|
-
`
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
function getHtmlTemplate() {
|
|
722
|
-
return `
|
|
723
|
-
<!DOCTYPE html>
|
|
724
|
-
<html lang="en">
|
|
725
|
-
<head>
|
|
726
|
-
<meta charset="UTF-8">
|
|
727
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
728
|
-
<title>{{title}}</title>
|
|
729
|
-
<style>{{cssStyles}}</style>
|
|
730
|
-
</head>
|
|
731
|
-
<body>
|
|
732
|
-
<header class="report-header">
|
|
733
|
-
<h1>{{title}}</h1>
|
|
734
|
-
<div class="report-meta">
|
|
735
|
-
<span>Generated: {{timestamp}}</span>
|
|
736
|
-
<span>Duration: {{duration}}</span>
|
|
737
|
-
</div>
|
|
738
|
-
</header>
|
|
739
|
-
|
|
740
|
-
<main class="report-content">
|
|
741
|
-
{{systemInfoHtml}}
|
|
742
|
-
|
|
743
|
-
<section class="stats-section">
|
|
744
|
-
<h2>Test Statistics</h2>
|
|
745
|
-
{{statsHtml}}
|
|
746
|
-
</section>
|
|
747
|
-
|
|
748
|
-
<section class="history-section" style="display: {{showHistory}};">
|
|
749
|
-
<h2>Test History</h2>
|
|
750
|
-
<div class="history-chart-container">
|
|
751
|
-
<canvas id="historyChart" width="800" height="300"></canvas>
|
|
752
|
-
</div>
|
|
753
|
-
</section>
|
|
754
|
-
|
|
755
|
-
<section class="filters-section">
|
|
756
|
-
<h2>Filters</h2>
|
|
757
|
-
<div class="filter-controls">
|
|
758
|
-
<div class="filter-group">
|
|
759
|
-
<label>Status:</label>
|
|
760
|
-
<select id="statusFilter" multiple>
|
|
761
|
-
<option value="passed">Passed</option>
|
|
762
|
-
<option value="failed">Failed</option>
|
|
763
|
-
<option value="pending">Pending</option>
|
|
764
|
-
<option value="skipped">Skipped</option>
|
|
765
|
-
</select>
|
|
766
|
-
</div>
|
|
767
|
-
<div class="filter-group">
|
|
768
|
-
<label>Feature:</label>
|
|
769
|
-
<input type="text" id="featureFilter" placeholder="Filter by feature...">
|
|
770
|
-
</div>
|
|
771
|
-
<div class="filter-group">
|
|
772
|
-
<label>Tags:</label>
|
|
773
|
-
<input type="text" id="tagFilter" placeholder="Filter by tags...">
|
|
774
|
-
</div>
|
|
775
|
-
<div class="filter-group">
|
|
776
|
-
<label>Retries:</label>
|
|
777
|
-
<select id="retryFilter">
|
|
778
|
-
<option value="all">All</option>
|
|
779
|
-
<option value="retried">With Retries</option>
|
|
780
|
-
<option value="no-retries">No Retries</option>
|
|
781
|
-
</select>
|
|
782
|
-
</div>
|
|
783
|
-
<div class="filter-group">
|
|
784
|
-
<label>Test Type:</label>
|
|
785
|
-
<select id="typeFilter">
|
|
786
|
-
<option value="all">All</option>
|
|
787
|
-
<option value="bdd">BDD/Gherkin</option>
|
|
788
|
-
<option value="regular">Regular</option>
|
|
789
|
-
</select>
|
|
790
|
-
</div>
|
|
791
|
-
<button onclick="resetFilters()">Reset Filters</button>
|
|
792
|
-
</div>
|
|
793
|
-
</section>
|
|
794
|
-
|
|
795
|
-
<section class="tests-section">
|
|
796
|
-
<h2>Test Results</h2>
|
|
797
|
-
<div class="tests-container">
|
|
798
|
-
{{testsHtml}}
|
|
799
|
-
</div>
|
|
800
|
-
</section>
|
|
801
|
-
|
|
802
|
-
<section class="retries-section" style="display: {{showRetries}};">
|
|
803
|
-
<h2>Test Retries</h2>
|
|
804
|
-
<div class="retries-container">
|
|
805
|
-
{{retriesHtml}}
|
|
806
|
-
</div>
|
|
807
|
-
</section>
|
|
808
|
-
|
|
809
|
-
<section class="failures-section" style="display: {{failuresDisplay}};">
|
|
810
|
-
<h2>Failures</h2>
|
|
811
|
-
<div class="failures-container">
|
|
812
|
-
{{failuresHtml}}
|
|
813
|
-
</div>
|
|
814
|
-
</section>
|
|
815
|
-
</main>
|
|
816
|
-
|
|
817
|
-
<!-- Modal for images -->
|
|
818
|
-
<div id="imageModal" class="modal" onclick="closeImageModal()">
|
|
819
|
-
<img id="modalImage" src="" alt="Enlarged screenshot"/>
|
|
820
|
-
</div>
|
|
821
|
-
|
|
822
|
-
<script>
|
|
823
|
-
window.testData = {
|
|
824
|
-
stats: {{stats}},
|
|
825
|
-
history: {{history}}
|
|
826
|
-
};
|
|
827
|
-
</script>
|
|
828
|
-
<script>{{jsScripts}}</script>
|
|
829
|
-
</body>
|
|
830
|
-
</html>
|
|
831
|
-
`
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function getCssStyles() {
|
|
835
|
-
return `
|
|
836
|
-
* {
|
|
837
|
-
margin: 0;
|
|
838
|
-
padding: 0;
|
|
839
|
-
box-sizing: border-box;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
body {
|
|
843
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
844
|
-
line-height: 1.6;
|
|
845
|
-
color: #333;
|
|
846
|
-
background-color: #f5f5f5;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
.report-header {
|
|
850
|
-
background: #2c3e50;
|
|
851
|
-
color: white;
|
|
852
|
-
padding: 2rem 1rem;
|
|
853
|
-
text-align: center;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
.report-header h1 {
|
|
857
|
-
margin-bottom: 0.5rem;
|
|
858
|
-
font-size: 2.5rem;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
.report-meta {
|
|
862
|
-
font-size: 0.9rem;
|
|
863
|
-
opacity: 0.8;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
.report-meta span {
|
|
867
|
-
margin: 0 1rem;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
.report-content {
|
|
871
|
-
max-width: 1200px;
|
|
872
|
-
margin: 2rem auto;
|
|
873
|
-
padding: 0 1rem;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
.stats-section, .tests-section, .failures-section, .retries-section, .filters-section, .history-section, .system-info-section {
|
|
877
|
-
background: white;
|
|
878
|
-
margin-bottom: 2rem;
|
|
879
|
-
border-radius: 8px;
|
|
880
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
881
|
-
overflow: hidden;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
.stats-section h2, .tests-section h2, .failures-section h2, .retries-section h2, .filters-section h2, .history-section h2 {
|
|
885
|
-
background: #34495e;
|
|
886
|
-
color: white;
|
|
887
|
-
padding: 1rem;
|
|
888
|
-
margin: 0;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
.stats-cards {
|
|
892
|
-
display: flex;
|
|
893
|
-
flex-wrap: wrap;
|
|
894
|
-
gap: 1rem;
|
|
895
|
-
padding: 1rem;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
.stat-card {
|
|
899
|
-
flex: 1;
|
|
900
|
-
min-width: 150px;
|
|
901
|
-
padding: 1rem;
|
|
902
|
-
text-align: center;
|
|
903
|
-
border-radius: 4px;
|
|
904
|
-
color: white;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
.stat-card.total { background: #3498db; }
|
|
908
|
-
.stat-card.passed { background: #27ae60; }
|
|
909
|
-
.stat-card.failed { background: #e74c3c; }
|
|
910
|
-
.stat-card.pending { background: #f39c12; }
|
|
911
|
-
|
|
912
|
-
.stat-card h3 {
|
|
913
|
-
font-size: 0.9rem;
|
|
914
|
-
margin-bottom: 0.5rem;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
.stat-number {
|
|
918
|
-
font-size: 2rem;
|
|
919
|
-
font-weight: bold;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
.pie-chart-container {
|
|
923
|
-
display: flex;
|
|
924
|
-
justify-content: center;
|
|
925
|
-
align-items: center;
|
|
926
|
-
padding: 2rem 1rem;
|
|
927
|
-
background: white;
|
|
928
|
-
margin: 1rem 0;
|
|
929
|
-
border-radius: 8px;
|
|
930
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
#statsChart {
|
|
934
|
-
max-width: 100%;
|
|
935
|
-
height: auto;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
.test-item {
|
|
939
|
-
border-bottom: 1px solid #eee;
|
|
940
|
-
margin: 0;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
.test-item:last-child {
|
|
944
|
-
border-bottom: none;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
.test-header {
|
|
948
|
-
display: flex;
|
|
949
|
-
align-items: center;
|
|
950
|
-
padding: 1rem;
|
|
951
|
-
cursor: pointer;
|
|
952
|
-
transition: background-color 0.2s;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
.test-header:hover {
|
|
956
|
-
background-color: #f8f9fa;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
.test-info {
|
|
960
|
-
flex: 1;
|
|
961
|
-
display: flex;
|
|
962
|
-
flex-direction: column;
|
|
963
|
-
gap: 0.25rem;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
.test-meta-line {
|
|
967
|
-
display: flex;
|
|
968
|
-
align-items: center;
|
|
969
|
-
gap: 0.5rem;
|
|
970
|
-
font-size: 0.9rem;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
.test-status {
|
|
974
|
-
font-size: 1.2rem;
|
|
975
|
-
margin-right: 0.5rem;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
.test-status.passed { color: #27ae60; }
|
|
979
|
-
.test-status.failed { color: #e74c3c; }
|
|
980
|
-
.test-status.pending { color: #f39c12; }
|
|
981
|
-
.test-status.skipped { color: #95a5a6; }
|
|
982
|
-
|
|
983
|
-
.test-title {
|
|
984
|
-
font-size: 1.1rem;
|
|
985
|
-
font-weight: 500;
|
|
986
|
-
margin: 0;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
.test-feature {
|
|
990
|
-
background: #ecf0f1;
|
|
991
|
-
padding: 0.25rem 0.5rem;
|
|
992
|
-
border-radius: 4px;
|
|
993
|
-
font-size: 0.8rem;
|
|
994
|
-
color: #34495e;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
.test-uid {
|
|
998
|
-
background: #e8f4fd;
|
|
999
|
-
padding: 0.25rem 0.5rem;
|
|
1000
|
-
border-radius: 4px;
|
|
1001
|
-
font-size: 0.7rem;
|
|
1002
|
-
color: #2980b9;
|
|
1003
|
-
font-family: monospace;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
.retry-badge {
|
|
1007
|
-
background: #f39c12;
|
|
1008
|
-
color: white;
|
|
1009
|
-
padding: 0.25rem 0.5rem;
|
|
1010
|
-
border-radius: 4px;
|
|
1011
|
-
font-size: 0.7rem;
|
|
1012
|
-
font-weight: bold;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
.test-duration {
|
|
1016
|
-
font-size: 0.8rem;
|
|
1017
|
-
color: #7f8c8d;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
.test-details {
|
|
1021
|
-
display: none;
|
|
1022
|
-
padding: 1rem;
|
|
1023
|
-
background: #f8f9fa;
|
|
1024
|
-
border-top: 1px solid #e9ecef;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
.error-message {
|
|
1028
|
-
background: #fee;
|
|
1029
|
-
border: 1px solid #fcc;
|
|
1030
|
-
border-radius: 4px;
|
|
1031
|
-
padding: 1rem;
|
|
1032
|
-
margin-bottom: 1rem;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
.error-message pre {
|
|
1036
|
-
color: #c0392b;
|
|
1037
|
-
font-family: 'Courier New', monospace;
|
|
1038
|
-
font-size: 0.9rem;
|
|
1039
|
-
white-space: pre-wrap;
|
|
1040
|
-
word-wrap: break-word;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
.steps-section, .artifacts-section, .hooks-section {
|
|
1044
|
-
margin-top: 1rem;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
.steps-section h4, .artifacts-section h4, .hooks-section h4 {
|
|
1048
|
-
color: #34495e;
|
|
1049
|
-
margin-bottom: 0.5rem;
|
|
1050
|
-
font-size: 1rem;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
.hook-item {
|
|
1054
|
-
display: flex;
|
|
1055
|
-
align-items: center;
|
|
1056
|
-
padding: 0.5rem 0;
|
|
1057
|
-
border-bottom: 1px solid #ecf0f1;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
.hook-item:last-child {
|
|
1061
|
-
border-bottom: none;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
.hook-status {
|
|
1065
|
-
margin-right: 0.5rem;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
.hook-status.passed { color: #27ae60; }
|
|
1069
|
-
.hook-status.failed { color: #e74c3c; }
|
|
1070
|
-
|
|
1071
|
-
.hook-title {
|
|
1072
|
-
flex: 1;
|
|
1073
|
-
font-family: 'Courier New', monospace;
|
|
1074
|
-
font-size: 0.9rem;
|
|
1075
|
-
font-weight: bold;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
.hook-duration {
|
|
1079
|
-
font-size: 0.8rem;
|
|
1080
|
-
color: #7f8c8d;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
.hook-error {
|
|
1084
|
-
width: 100%;
|
|
1085
|
-
margin-top: 0.5rem;
|
|
1086
|
-
padding: 0.5rem;
|
|
1087
|
-
background: #fee;
|
|
1088
|
-
border: 1px solid #fcc;
|
|
1089
|
-
border-radius: 4px;
|
|
1090
|
-
color: #c0392b;
|
|
1091
|
-
font-size: 0.8rem;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
.step-item {
|
|
1095
|
-
display: flex;
|
|
1096
|
-
align-items: center;
|
|
1097
|
-
padding: 0.5rem 0;
|
|
1098
|
-
border-bottom: 1px solid #ecf0f1;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
.step-item:last-child {
|
|
1102
|
-
border-bottom: none;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
.step-status {
|
|
1106
|
-
margin-right: 0.5rem;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
.step-status.success { color: #27ae60; }
|
|
1110
|
-
.step-status.failed { color: #e74c3c; }
|
|
1111
|
-
|
|
1112
|
-
.step-title {
|
|
1113
|
-
flex: 1;
|
|
1114
|
-
font-family: 'Courier New', monospace;
|
|
1115
|
-
font-size: 0.9rem;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
.step-duration {
|
|
1119
|
-
font-size: 0.8rem;
|
|
1120
|
-
color: #7f8c8d;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
.artifacts-list {
|
|
1124
|
-
display: flex;
|
|
1125
|
-
flex-wrap: wrap;
|
|
1126
|
-
gap: 0.5rem;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
.artifact-image {
|
|
1130
|
-
max-width: 200px;
|
|
1131
|
-
max-height: 150px;
|
|
1132
|
-
border: 1px solid #ddd;
|
|
1133
|
-
border-radius: 4px;
|
|
1134
|
-
cursor: pointer;
|
|
1135
|
-
transition: transform 0.2s;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
.artifact-image:hover {
|
|
1139
|
-
transform: scale(1.05);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
.artifact-item {
|
|
1143
|
-
background: #ecf0f1;
|
|
1144
|
-
padding: 0.5rem;
|
|
1145
|
-
border-radius: 4px;
|
|
1146
|
-
font-size: 0.9rem;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
.modal {
|
|
1150
|
-
display: none;
|
|
1151
|
-
position: fixed;
|
|
1152
|
-
z-index: 1000;
|
|
1153
|
-
left: 0;
|
|
1154
|
-
top: 0;
|
|
1155
|
-
width: 100%;
|
|
1156
|
-
height: 100%;
|
|
1157
|
-
background-color: rgba(0,0,0,0.8);
|
|
1158
|
-
cursor: pointer;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
.modal img {
|
|
1162
|
-
position: absolute;
|
|
1163
|
-
top: 50%;
|
|
1164
|
-
left: 50%;
|
|
1165
|
-
transform: translate(-50%, -50%);
|
|
1166
|
-
max-width: 90%;
|
|
1167
|
-
max-height: 90%;
|
|
1168
|
-
border-radius: 4px;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
.failure-item {
|
|
1172
|
-
padding: 1rem;
|
|
1173
|
-
margin-bottom: 1rem;
|
|
1174
|
-
border: 1px solid #fcc;
|
|
1175
|
-
border-radius: 4px;
|
|
1176
|
-
background: #fee;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
.failure-item h4 {
|
|
1180
|
-
color: #c0392b;
|
|
1181
|
-
margin-bottom: 0.5rem;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
.failure-details {
|
|
1185
|
-
color: #333;
|
|
1186
|
-
font-family: 'Courier New', monospace;
|
|
1187
|
-
font-size: 0.9rem;
|
|
1188
|
-
white-space: pre-wrap;
|
|
1189
|
-
word-wrap: break-word;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
/* Filter Controls */
|
|
1193
|
-
.filter-controls {
|
|
1194
|
-
display: flex;
|
|
1195
|
-
flex-wrap: wrap;
|
|
1196
|
-
gap: 1rem;
|
|
1197
|
-
padding: 1rem;
|
|
1198
|
-
background: #f8f9fa;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
.filter-group {
|
|
1202
|
-
display: flex;
|
|
1203
|
-
flex-direction: column;
|
|
1204
|
-
gap: 0.25rem;
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
.filter-group label {
|
|
1208
|
-
font-size: 0.9rem;
|
|
1209
|
-
font-weight: 500;
|
|
1210
|
-
color: #34495e;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
.filter-group input,
|
|
1214
|
-
.filter-group select {
|
|
1215
|
-
padding: 0.5rem;
|
|
1216
|
-
border: 1px solid #ddd;
|
|
1217
|
-
border-radius: 4px;
|
|
1218
|
-
font-size: 0.9rem;
|
|
1219
|
-
min-width: 150px;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
.filter-group select[multiple] {
|
|
1223
|
-
height: auto;
|
|
1224
|
-
min-height: 80px;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
.filter-controls button {
|
|
1228
|
-
padding: 0.5rem 1rem;
|
|
1229
|
-
background: #3498db;
|
|
1230
|
-
color: white;
|
|
1231
|
-
border: none;
|
|
1232
|
-
border-radius: 4px;
|
|
1233
|
-
cursor: pointer;
|
|
1234
|
-
font-size: 0.9rem;
|
|
1235
|
-
align-self: flex-end;
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
.filter-controls button:hover {
|
|
1239
|
-
background: #2980b9;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
/* Test Tags */
|
|
1243
|
-
.tags-section, .metadata-section, .notes-section, .retry-section {
|
|
1244
|
-
margin-top: 1rem;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
.tags-list {
|
|
1248
|
-
display: flex;
|
|
1249
|
-
flex-wrap: wrap;
|
|
1250
|
-
gap: 0.5rem;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
.test-tag {
|
|
1254
|
-
background: #3498db;
|
|
1255
|
-
color: white;
|
|
1256
|
-
padding: 0.25rem 0.5rem;
|
|
1257
|
-
border-radius: 12px;
|
|
1258
|
-
font-size: 0.8rem;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
/* Metadata */
|
|
1262
|
-
.metadata-list {
|
|
1263
|
-
display: flex;
|
|
1264
|
-
flex-direction: column;
|
|
1265
|
-
gap: 0.5rem;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
.meta-item {
|
|
1269
|
-
padding: 0.5rem;
|
|
1270
|
-
background: #f8f9fa;
|
|
1271
|
-
border-radius: 4px;
|
|
1272
|
-
border-left: 3px solid #3498db;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
.meta-key {
|
|
1276
|
-
font-weight: bold;
|
|
1277
|
-
color: #2c3e50;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
.meta-value {
|
|
1281
|
-
color: #34495e;
|
|
1282
|
-
font-family: monospace;
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
/* Notes */
|
|
1286
|
-
.notes-list {
|
|
1287
|
-
display: flex;
|
|
1288
|
-
flex-direction: column;
|
|
1289
|
-
gap: 0.5rem;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
.note-item {
|
|
1293
|
-
padding: 0.5rem;
|
|
1294
|
-
border-radius: 4px;
|
|
1295
|
-
border-left: 3px solid #95a5a6;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
.note-item.note-info {
|
|
1299
|
-
background: #e8f4fd;
|
|
1300
|
-
border-left-color: #3498db;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
.note-item.note-warning {
|
|
1304
|
-
background: #fef9e7;
|
|
1305
|
-
border-left-color: #f39c12;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
.note-item.note-error {
|
|
1309
|
-
background: #fee;
|
|
1310
|
-
border-left-color: #e74c3c;
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
.note-item.note-retry {
|
|
1314
|
-
background: #f0f8e8;
|
|
1315
|
-
border-left-color: #27ae60;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
.note-type {
|
|
1319
|
-
font-weight: bold;
|
|
1320
|
-
text-transform: uppercase;
|
|
1321
|
-
font-size: 0.8rem;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
/* Retry Information */
|
|
1325
|
-
.retry-info {
|
|
1326
|
-
padding: 0.5rem;
|
|
1327
|
-
background: #fef9e7;
|
|
1328
|
-
border-radius: 4px;
|
|
1329
|
-
border-left: 3px solid #f39c12;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
.retry-count {
|
|
1333
|
-
color: #d68910;
|
|
1334
|
-
font-weight: 500;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
/* Retries Section */
|
|
1338
|
-
.retry-item {
|
|
1339
|
-
padding: 1rem;
|
|
1340
|
-
margin-bottom: 1rem;
|
|
1341
|
-
border: 1px solid #f39c12;
|
|
1342
|
-
border-radius: 4px;
|
|
1343
|
-
background: #fef9e7;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
.retry-item h4 {
|
|
1347
|
-
color: #d68910;
|
|
1348
|
-
margin-bottom: 0.5rem;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
.retry-details {
|
|
1352
|
-
display: flex;
|
|
1353
|
-
gap: 1rem;
|
|
1354
|
-
align-items: center;
|
|
1355
|
-
font-size: 0.9rem;
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
.status-badge {
|
|
1359
|
-
padding: 0.25rem 0.5rem;
|
|
1360
|
-
border-radius: 4px;
|
|
1361
|
-
font-size: 0.8rem;
|
|
1362
|
-
font-weight: bold;
|
|
1363
|
-
text-transform: uppercase;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
.status-badge.passed {
|
|
1367
|
-
background: #27ae60;
|
|
1368
|
-
color: white;
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
.status-badge.failed {
|
|
1372
|
-
background: #e74c3c;
|
|
1373
|
-
color: white;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
.status-badge.pending {
|
|
1377
|
-
background: #f39c12;
|
|
1378
|
-
color: white;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
/* History Chart */
|
|
1382
|
-
.history-chart-container {
|
|
1383
|
-
padding: 2rem 1rem;
|
|
1384
|
-
display: flex;
|
|
1385
|
-
justify-content: center;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
#historyChart {
|
|
1389
|
-
max-width: 100%;
|
|
1390
|
-
height: auto;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
/* Hidden items for filtering */
|
|
1394
|
-
.test-item.filtered-out {
|
|
1395
|
-
display: none !important;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
/* System Info Section */
|
|
1399
|
-
.system-info-section {
|
|
1400
|
-
background: white;
|
|
1401
|
-
margin-bottom: 2rem;
|
|
1402
|
-
border-radius: 8px;
|
|
1403
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
1404
|
-
overflow: hidden;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
.system-info-header {
|
|
1408
|
-
background: #2c3e50;
|
|
1409
|
-
color: white;
|
|
1410
|
-
padding: 1rem;
|
|
1411
|
-
cursor: pointer;
|
|
1412
|
-
display: flex;
|
|
1413
|
-
justify-content: space-between;
|
|
1414
|
-
align-items: center;
|
|
1415
|
-
transition: background-color 0.2s;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
.system-info-header:hover {
|
|
1419
|
-
background: #34495e;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
.system-info-header h3 {
|
|
1423
|
-
margin: 0;
|
|
1424
|
-
font-size: 1.2rem;
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
.toggle-icon {
|
|
1428
|
-
font-size: 1rem;
|
|
1429
|
-
transition: transform 0.3s ease;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
.toggle-icon.rotated {
|
|
1433
|
-
transform: rotate(-180deg);
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
.system-info-content {
|
|
1437
|
-
display: none;
|
|
1438
|
-
padding: 1.5rem;
|
|
1439
|
-
background: #f8f9fa;
|
|
1440
|
-
border-top: 1px solid #e9ecef;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
.system-info-content.visible {
|
|
1444
|
-
display: block;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
.system-info-grid {
|
|
1448
|
-
display: grid;
|
|
1449
|
-
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
1450
|
-
gap: 1rem;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
.info-item {
|
|
1454
|
-
padding: 0.75rem;
|
|
1455
|
-
background: white;
|
|
1456
|
-
border-radius: 6px;
|
|
1457
|
-
border-left: 4px solid #3498db;
|
|
1458
|
-
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
.info-key {
|
|
1462
|
-
font-weight: bold;
|
|
1463
|
-
color: #2c3e50;
|
|
1464
|
-
display: inline-block;
|
|
1465
|
-
min-width: 100px;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
.info-value {
|
|
1469
|
-
color: #34495e;
|
|
1470
|
-
font-family: 'Courier New', monospace;
|
|
1471
|
-
font-size: 0.9rem;
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
/* BDD/Gherkin specific styles */
|
|
1475
|
-
.bdd-test {
|
|
1476
|
-
border-left: 4px solid #8e44ad;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
.bdd-badge {
|
|
1480
|
-
background: #8e44ad;
|
|
1481
|
-
color: white;
|
|
1482
|
-
padding: 0.25rem 0.5rem;
|
|
1483
|
-
border-radius: 4px;
|
|
1484
|
-
font-size: 0.7rem;
|
|
1485
|
-
font-weight: bold;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
.bdd-feature-section {
|
|
1489
|
-
margin-top: 1rem;
|
|
1490
|
-
padding: 1rem;
|
|
1491
|
-
background: #f8f9fa;
|
|
1492
|
-
border-left: 4px solid #8e44ad;
|
|
1493
|
-
border-radius: 4px;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
.feature-name {
|
|
1497
|
-
font-weight: bold;
|
|
1498
|
-
font-size: 1.1rem;
|
|
1499
|
-
color: #8e44ad;
|
|
1500
|
-
margin-bottom: 0.5rem;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
.feature-description {
|
|
1504
|
-
color: #34495e;
|
|
1505
|
-
font-style: italic;
|
|
1506
|
-
margin: 0.5rem 0;
|
|
1507
|
-
padding: 0.5rem;
|
|
1508
|
-
background: white;
|
|
1509
|
-
border-radius: 4px;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
.feature-file {
|
|
1513
|
-
font-size: 0.8rem;
|
|
1514
|
-
color: #7f8c8d;
|
|
1515
|
-
margin-top: 0.5rem;
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
.feature-tags {
|
|
1519
|
-
display: flex;
|
|
1520
|
-
flex-wrap: wrap;
|
|
1521
|
-
gap: 0.25rem;
|
|
1522
|
-
margin: 0.5rem 0;
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
.feature-tag {
|
|
1526
|
-
background: #8e44ad;
|
|
1527
|
-
color: white;
|
|
1528
|
-
padding: 0.2rem 0.4rem;
|
|
1529
|
-
border-radius: 8px;
|
|
1530
|
-
font-size: 0.7rem;
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
.bdd-steps-section {
|
|
1534
|
-
margin-top: 1rem;
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
.bdd-steps-section h4 {
|
|
1538
|
-
color: #8e44ad;
|
|
1539
|
-
margin-bottom: 0.5rem;
|
|
1540
|
-
font-size: 1rem;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
.bdd-step-item {
|
|
1544
|
-
display: flex;
|
|
1545
|
-
align-items: flex-start;
|
|
1546
|
-
padding: 0.5rem 0;
|
|
1547
|
-
border-bottom: 1px solid #ecf0f1;
|
|
1548
|
-
font-family: 'Segoe UI', sans-serif;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
.bdd-step-item:last-child {
|
|
1552
|
-
border-bottom: none;
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
.bdd-keyword {
|
|
1556
|
-
font-weight: bold;
|
|
1557
|
-
color: #8e44ad;
|
|
1558
|
-
margin-right: 0.5rem;
|
|
1559
|
-
min-width: 60px;
|
|
1560
|
-
text-align: left;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
.bdd-step-text {
|
|
1564
|
-
flex: 1;
|
|
1565
|
-
color: #2c3e50;
|
|
1566
|
-
margin-right: 0.5rem;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
.step-comment {
|
|
1570
|
-
width: 100%;
|
|
1571
|
-
margin-top: 0.5rem;
|
|
1572
|
-
padding: 0.5rem;
|
|
1573
|
-
background: #f8f9fa;
|
|
1574
|
-
border: 1px solid #e9ecef;
|
|
1575
|
-
border-radius: 4px;
|
|
1576
|
-
color: #6c757d;
|
|
1577
|
-
font-family: 'Courier New', monospace;
|
|
1578
|
-
font-size: 0.8rem;
|
|
1579
|
-
white-space: pre-wrap;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
@media (max-width: 768px) {
|
|
1583
|
-
.stats-cards {
|
|
1584
|
-
flex-direction: column;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
.test-header {
|
|
1588
|
-
flex-direction: column;
|
|
1589
|
-
align-items: stretch;
|
|
1590
|
-
gap: 0.5rem;
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
.test-feature, .test-duration {
|
|
1594
|
-
align-self: flex-start;
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
`
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
function getJsScripts() {
|
|
1601
|
-
return `
|
|
1602
|
-
function toggleTestDetails(testId) {
|
|
1603
|
-
const details = document.getElementById('details-' + testId);
|
|
1604
|
-
if (details.style.display === 'none' || details.style.display === '') {
|
|
1605
|
-
details.style.display = 'block';
|
|
1606
|
-
} else {
|
|
1607
|
-
details.style.display = 'none';
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
function openImageModal(src) {
|
|
1612
|
-
const modal = document.getElementById('imageModal');
|
|
1613
|
-
const modalImg = document.getElementById('modalImage');
|
|
1614
|
-
modalImg.src = src;
|
|
1615
|
-
modal.style.display = 'block';
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
function closeImageModal() {
|
|
1619
|
-
const modal = document.getElementById('imageModal');
|
|
1620
|
-
modal.style.display = 'none';
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
function toggleSystemInfo() {
|
|
1624
|
-
const content = document.getElementById('systemInfoContent');
|
|
1625
|
-
const icon = document.querySelector('.toggle-icon');
|
|
1626
|
-
|
|
1627
|
-
if (content.classList.contains('visible')) {
|
|
1628
|
-
content.classList.remove('visible');
|
|
1629
|
-
icon.classList.remove('rotated');
|
|
1630
|
-
} else {
|
|
1631
|
-
content.classList.add('visible');
|
|
1632
|
-
icon.classList.add('rotated');
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// Filter functionality
|
|
1637
|
-
function applyFilters() {
|
|
1638
|
-
const statusFilter = Array.from(document.getElementById('statusFilter').selectedOptions).map(opt => opt.value);
|
|
1639
|
-
const featureFilter = document.getElementById('featureFilter').value.toLowerCase();
|
|
1640
|
-
const tagFilter = document.getElementById('tagFilter').value.toLowerCase();
|
|
1641
|
-
const retryFilter = document.getElementById('retryFilter').value;
|
|
1642
|
-
const typeFilter = document.getElementById('typeFilter').value;
|
|
1643
|
-
|
|
1644
|
-
const testItems = document.querySelectorAll('.test-item');
|
|
1645
|
-
|
|
1646
|
-
testItems.forEach(item => {
|
|
1647
|
-
let shouldShow = true;
|
|
1648
|
-
|
|
1649
|
-
// Status filter
|
|
1650
|
-
if (statusFilter.length > 0) {
|
|
1651
|
-
const testStatus = item.dataset.status;
|
|
1652
|
-
if (!statusFilter.includes(testStatus)) {
|
|
1653
|
-
shouldShow = false;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
// Feature filter
|
|
1658
|
-
if (featureFilter && shouldShow) {
|
|
1659
|
-
const feature = (item.dataset.feature || '').toLowerCase();
|
|
1660
|
-
if (!feature.includes(featureFilter)) {
|
|
1661
|
-
shouldShow = false;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
// Tag filter
|
|
1666
|
-
if (tagFilter && shouldShow) {
|
|
1667
|
-
const tags = (item.dataset.tags || '').toLowerCase();
|
|
1668
|
-
if (!tags.includes(tagFilter)) {
|
|
1669
|
-
shouldShow = false;
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
// Retry filter
|
|
1674
|
-
if (retryFilter !== 'all' && shouldShow) {
|
|
1675
|
-
const retries = parseInt(item.dataset.retries || '0');
|
|
1676
|
-
if (retryFilter === 'retried' && retries === 0) {
|
|
1677
|
-
shouldShow = false;
|
|
1678
|
-
} else if (retryFilter === 'no-retries' && retries > 0) {
|
|
1679
|
-
shouldShow = false;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
// Test type filter (BDD/Gherkin vs Regular)
|
|
1684
|
-
if (typeFilter !== 'all' && shouldShow) {
|
|
1685
|
-
const testType = item.dataset.type || 'regular';
|
|
1686
|
-
if (typeFilter !== testType) {
|
|
1687
|
-
shouldShow = false;
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
if (shouldShow) {
|
|
1692
|
-
item.classList.remove('filtered-out');
|
|
1693
|
-
} else {
|
|
1694
|
-
item.classList.add('filtered-out');
|
|
1695
|
-
}
|
|
1696
|
-
});
|
|
1697
|
-
|
|
1698
|
-
updateFilteredStats();
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
function resetFilters() {
|
|
1702
|
-
document.getElementById('statusFilter').selectedIndex = -1;
|
|
1703
|
-
document.getElementById('featureFilter').value = '';
|
|
1704
|
-
document.getElementById('tagFilter').value = '';
|
|
1705
|
-
document.getElementById('retryFilter').value = 'all';
|
|
1706
|
-
document.getElementById('typeFilter').value = 'all';
|
|
1707
|
-
|
|
1708
|
-
document.querySelectorAll('.test-item').forEach(item => {
|
|
1709
|
-
item.classList.remove('filtered-out');
|
|
1710
|
-
});
|
|
1711
|
-
|
|
1712
|
-
updateFilteredStats();
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
function updateFilteredStats() {
|
|
1716
|
-
const visibleTests = document.querySelectorAll('.test-item:not(.filtered-out)');
|
|
1717
|
-
const totalVisible = visibleTests.length;
|
|
1718
|
-
|
|
1719
|
-
// Update the title to show filtered count
|
|
1720
|
-
const testsSection = document.querySelector('.tests-section h2');
|
|
1721
|
-
const totalTests = document.querySelectorAll('.test-item').length;
|
|
1722
|
-
|
|
1723
|
-
if (totalVisible !== totalTests) {
|
|
1724
|
-
testsSection.textContent = 'Test Results (' + totalVisible + ' of ' + totalTests + ' shown)';
|
|
1725
|
-
} else {
|
|
1726
|
-
testsSection.textContent = 'Test Results';
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
// Draw pie chart using canvas
|
|
1731
|
-
function drawPieChart() {
|
|
1732
|
-
const canvas = document.getElementById('statsChart');
|
|
1733
|
-
if (!canvas) return;
|
|
1734
|
-
|
|
1735
|
-
const ctx = canvas.getContext('2d');
|
|
1736
|
-
const data = window.chartData;
|
|
1737
|
-
|
|
1738
|
-
if (!data) return;
|
|
1739
|
-
|
|
1740
|
-
const centerX = canvas.width / 2;
|
|
1741
|
-
const centerY = canvas.height / 2;
|
|
1742
|
-
const radius = Math.min(centerX, centerY) - 20;
|
|
1743
|
-
|
|
1744
|
-
const total = data.passed + data.failed + data.pending;
|
|
1745
|
-
if (total === 0) {
|
|
1746
|
-
// Draw empty circle for no tests
|
|
1747
|
-
ctx.strokeStyle = '#ddd';
|
|
1748
|
-
ctx.lineWidth = 2;
|
|
1749
|
-
ctx.beginPath();
|
|
1750
|
-
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
|
1751
|
-
ctx.stroke();
|
|
1752
|
-
ctx.fillStyle = '#888';
|
|
1753
|
-
ctx.font = '16px Arial';
|
|
1754
|
-
ctx.textAlign = 'center';
|
|
1755
|
-
ctx.fillText('No Tests', centerX, centerY);
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
let currentAngle = -Math.PI / 2; // Start from top
|
|
1760
|
-
|
|
1761
|
-
// Draw passed segment
|
|
1762
|
-
if (data.passed > 0) {
|
|
1763
|
-
const angle = (data.passed / total) * 2 * Math.PI;
|
|
1764
|
-
ctx.beginPath();
|
|
1765
|
-
ctx.moveTo(centerX, centerY);
|
|
1766
|
-
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + angle);
|
|
1767
|
-
ctx.closePath();
|
|
1768
|
-
ctx.fillStyle = '#27ae60';
|
|
1769
|
-
ctx.fill();
|
|
1770
|
-
currentAngle += angle;
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
// Draw failed segment
|
|
1774
|
-
if (data.failed > 0) {
|
|
1775
|
-
const angle = (data.failed / total) * 2 * Math.PI;
|
|
1776
|
-
ctx.beginPath();
|
|
1777
|
-
ctx.moveTo(centerX, centerY);
|
|
1778
|
-
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + angle);
|
|
1779
|
-
ctx.closePath();
|
|
1780
|
-
ctx.fillStyle = '#e74c3c';
|
|
1781
|
-
ctx.fill();
|
|
1782
|
-
currentAngle += angle;
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
// Draw pending segment
|
|
1786
|
-
if (data.pending > 0) {
|
|
1787
|
-
const angle = (data.pending / total) * 2 * Math.PI;
|
|
1788
|
-
ctx.beginPath();
|
|
1789
|
-
ctx.moveTo(centerX, centerY);
|
|
1790
|
-
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + angle);
|
|
1791
|
-
ctx.closePath();
|
|
1792
|
-
ctx.fillStyle = '#f39c12';
|
|
1793
|
-
ctx.fill();
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
// Add legend
|
|
1797
|
-
const legendY = centerY + radius + 40;
|
|
1798
|
-
ctx.font = '14px Arial';
|
|
1799
|
-
ctx.textAlign = 'left';
|
|
1800
|
-
|
|
1801
|
-
let legendX = centerX - 120;
|
|
1802
|
-
|
|
1803
|
-
// Passed legend
|
|
1804
|
-
ctx.fillStyle = '#27ae60';
|
|
1805
|
-
ctx.fillRect(legendX, legendY, 15, 15);
|
|
1806
|
-
ctx.fillStyle = '#333';
|
|
1807
|
-
ctx.fillText('Passed (' + data.passed + ')', legendX + 20, legendY + 12);
|
|
1808
|
-
|
|
1809
|
-
// Failed legend
|
|
1810
|
-
legendX += 100;
|
|
1811
|
-
ctx.fillStyle = '#e74c3c';
|
|
1812
|
-
ctx.fillRect(legendX, legendY, 15, 15);
|
|
1813
|
-
ctx.fillStyle = '#333';
|
|
1814
|
-
ctx.fillText('Failed (' + data.failed + ')', legendX + 20, legendY + 12);
|
|
1815
|
-
|
|
1816
|
-
// Pending legend
|
|
1817
|
-
if (data.pending > 0) {
|
|
1818
|
-
legendX += 90;
|
|
1819
|
-
ctx.fillStyle = '#f39c12';
|
|
1820
|
-
ctx.fillRect(legendX, legendY, 15, 15);
|
|
1821
|
-
ctx.fillStyle = '#333';
|
|
1822
|
-
ctx.fillText('Pending (' + data.pending + ')', legendX + 20, legendY + 12);
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
// Draw history chart
|
|
1827
|
-
function drawHistoryChart() {
|
|
1828
|
-
const canvas = document.getElementById('historyChart');
|
|
1829
|
-
if (!canvas || !window.testData.history || window.testData.history.length === 0) return;
|
|
1830
|
-
|
|
1831
|
-
const ctx = canvas.getContext('2d');
|
|
1832
|
-
const history = window.testData.history.slice().reverse(); // Most recent last
|
|
1833
|
-
|
|
1834
|
-
const padding = 50;
|
|
1835
|
-
const chartWidth = canvas.width - 2 * padding;
|
|
1836
|
-
const chartHeight = canvas.height - 2 * padding;
|
|
1837
|
-
|
|
1838
|
-
// Clear canvas
|
|
1839
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
1840
|
-
|
|
1841
|
-
// Find max values
|
|
1842
|
-
const maxTests = Math.max(...history.map(h => h.stats.tests || 0));
|
|
1843
|
-
const maxDuration = Math.max(...history.map(h => h.duration || 0));
|
|
1844
|
-
|
|
1845
|
-
if (maxTests === 0) return;
|
|
1846
|
-
|
|
1847
|
-
// Draw axes
|
|
1848
|
-
ctx.strokeStyle = '#333';
|
|
1849
|
-
ctx.lineWidth = 1;
|
|
1850
|
-
ctx.beginPath();
|
|
1851
|
-
ctx.moveTo(padding, padding);
|
|
1852
|
-
ctx.lineTo(padding, canvas.height - padding);
|
|
1853
|
-
ctx.lineTo(canvas.width - padding, canvas.height - padding);
|
|
1854
|
-
ctx.stroke();
|
|
1855
|
-
|
|
1856
|
-
// Draw grid lines
|
|
1857
|
-
ctx.strokeStyle = '#eee';
|
|
1858
|
-
ctx.lineWidth = 1;
|
|
1859
|
-
for (let i = 1; i <= 5; i++) {
|
|
1860
|
-
const y = padding + (chartHeight * i / 5);
|
|
1861
|
-
ctx.beginPath();
|
|
1862
|
-
ctx.moveTo(padding, y);
|
|
1863
|
-
ctx.lineTo(canvas.width - padding, y);
|
|
1864
|
-
ctx.stroke();
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
// Draw pass/fail rates
|
|
1868
|
-
const stepX = chartWidth / (history.length - 1);
|
|
1869
|
-
|
|
1870
|
-
// Draw passed tests line
|
|
1871
|
-
ctx.strokeStyle = '#27ae60';
|
|
1872
|
-
ctx.lineWidth = 3;
|
|
1873
|
-
ctx.beginPath();
|
|
1874
|
-
history.forEach((run, index) => {
|
|
1875
|
-
const x = padding + (index * stepX);
|
|
1876
|
-
const y = canvas.height - padding - ((run.stats.passes || 0) / maxTests) * chartHeight;
|
|
1877
|
-
if (index === 0) {
|
|
1878
|
-
ctx.moveTo(x, y);
|
|
1879
|
-
} else {
|
|
1880
|
-
ctx.lineTo(x, y);
|
|
1881
|
-
}
|
|
1882
|
-
});
|
|
1883
|
-
ctx.stroke();
|
|
1884
|
-
|
|
1885
|
-
// Draw failed tests line
|
|
1886
|
-
ctx.strokeStyle = '#e74c3c';
|
|
1887
|
-
ctx.lineWidth = 3;
|
|
1888
|
-
ctx.beginPath();
|
|
1889
|
-
history.forEach((run, index) => {
|
|
1890
|
-
const x = padding + (index * stepX);
|
|
1891
|
-
const y = canvas.height - padding - ((run.stats.failures || 0) / maxTests) * chartHeight;
|
|
1892
|
-
if (index === 0) {
|
|
1893
|
-
ctx.moveTo(x, y);
|
|
1894
|
-
} else {
|
|
1895
|
-
ctx.lineTo(x, y);
|
|
1896
|
-
}
|
|
1897
|
-
});
|
|
1898
|
-
ctx.stroke();
|
|
1899
|
-
|
|
1900
|
-
// Add labels
|
|
1901
|
-
ctx.fillStyle = '#333';
|
|
1902
|
-
ctx.font = '12px Arial';
|
|
1903
|
-
ctx.textAlign = 'center';
|
|
1904
|
-
|
|
1905
|
-
// Y-axis labels
|
|
1906
|
-
ctx.textAlign = 'right';
|
|
1907
|
-
for (let i = 0; i <= 5; i++) {
|
|
1908
|
-
const value = Math.round((maxTests * i) / 5);
|
|
1909
|
-
const y = canvas.height - padding - (chartHeight * i / 5);
|
|
1910
|
-
ctx.fillText(value.toString(), padding - 10, y + 4);
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
// Legend
|
|
1914
|
-
ctx.textAlign = 'left';
|
|
1915
|
-
ctx.fillStyle = '#27ae60';
|
|
1916
|
-
ctx.fillRect(padding, 20, 15, 15);
|
|
1917
|
-
ctx.fillStyle = '#333';
|
|
1918
|
-
ctx.fillText('Passed Tests', padding + 20, 32);
|
|
1919
|
-
|
|
1920
|
-
ctx.fillStyle = '#e74c3c';
|
|
1921
|
-
ctx.fillRect(padding + 120, 20, 15, 15);
|
|
1922
|
-
ctx.fillStyle = '#333';
|
|
1923
|
-
ctx.fillText('Failed Tests', padding + 140, 32);
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
// Initialize - hide failures section if no failures and draw charts
|
|
1927
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
1928
|
-
const failuresSection = document.querySelector('.failures-section');
|
|
1929
|
-
const failureItems = document.querySelectorAll('.failure-item');
|
|
1930
|
-
if (failureItems.length === 0) {
|
|
1931
|
-
failuresSection.style.display = 'none';
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
// Draw charts
|
|
1935
|
-
drawPieChart();
|
|
1936
|
-
drawHistoryChart();
|
|
1937
|
-
|
|
1938
|
-
// Set up filter event listeners
|
|
1939
|
-
document.getElementById('statusFilter').addEventListener('change', applyFilters);
|
|
1940
|
-
document.getElementById('featureFilter').addEventListener('input', applyFilters);
|
|
1941
|
-
document.getElementById('tagFilter').addEventListener('input', applyFilters);
|
|
1942
|
-
document.getElementById('retryFilter').addEventListener('change', applyFilters);
|
|
1943
|
-
document.getElementById('typeFilter').addEventListener('change', applyFilters);
|
|
1944
|
-
});
|
|
1945
|
-
`
|
|
1946
|
-
}
|
|
1947
|
-
}
|