codeceptjs 4.0.0-beta.2 → 4.0.0-beta.20
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 +133 -120
- package/bin/codecept.js +107 -96
- package/bin/test-server.js +64 -0
- package/docs/webapi/clearCookie.mustache +1 -1
- package/docs/webapi/click.mustache +5 -1
- package/lib/actor.js +71 -103
- package/lib/ai.js +159 -188
- package/lib/assert/empty.js +22 -24
- package/lib/assert/equal.js +30 -37
- package/lib/assert/error.js +14 -14
- package/lib/assert/include.js +43 -48
- package/lib/assert/throws.js +11 -11
- package/lib/assert/truth.js +22 -22
- package/lib/assert.js +20 -18
- package/lib/codecept.js +262 -162
- package/lib/colorUtils.js +50 -52
- package/lib/command/check.js +206 -0
- package/lib/command/configMigrate.js +56 -51
- package/lib/command/definitions.js +96 -109
- package/lib/command/dryRun.js +77 -79
- package/lib/command/generate.js +234 -194
- package/lib/command/gherkin/init.js +42 -33
- package/lib/command/gherkin/snippets.js +76 -74
- package/lib/command/gherkin/steps.js +20 -17
- package/lib/command/info.js +74 -38
- package/lib/command/init.js +301 -290
- package/lib/command/interactive.js +41 -32
- package/lib/command/list.js +28 -27
- package/lib/command/run-multiple/chunk.js +51 -48
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +5 -1
- package/lib/command/run-multiple.js +97 -97
- package/lib/command/run-rerun.js +19 -25
- package/lib/command/run-workers.js +68 -92
- package/lib/command/run.js +39 -27
- package/lib/command/utils.js +80 -64
- package/lib/command/workers/runTests.js +388 -226
- package/lib/config.js +109 -50
- package/lib/container.js +641 -261
- package/lib/data/context.js +60 -61
- package/lib/data/dataScenarioConfig.js +47 -47
- package/lib/data/dataTableArgument.js +32 -32
- package/lib/data/table.js +22 -22
- package/lib/effects.js +307 -0
- package/lib/element/WebElement.js +327 -0
- package/lib/els.js +160 -0
- package/lib/event.js +173 -163
- package/lib/globals.js +141 -0
- package/lib/heal.js +89 -85
- package/lib/helper/AI.js +131 -41
- package/lib/helper/ApiDataFactory.js +107 -75
- package/lib/helper/Appium.js +542 -404
- package/lib/helper/FileSystem.js +100 -79
- package/lib/helper/GraphQL.js +44 -43
- package/lib/helper/GraphQLDataFactory.js +52 -52
- package/lib/helper/JSONResponse.js +126 -88
- package/lib/helper/Mochawesome.js +54 -29
- package/lib/helper/Playwright.js +2547 -1316
- package/lib/helper/Puppeteer.js +1578 -1181
- package/lib/helper/REST.js +209 -68
- package/lib/helper/WebDriver.js +1482 -1342
- 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/PlaywrightReactVueLocator.js +17 -8
- package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
- package/lib/helper/extras/Popup.js +22 -22
- package/lib/helper/extras/React.js +27 -28
- package/lib/helper/network/actions.js +36 -42
- package/lib/helper/network/utils.js +78 -84
- package/lib/helper/scripts/blurElement.js +5 -5
- package/lib/helper/scripts/focusElement.js +5 -5
- package/lib/helper/scripts/highlightElement.js +8 -8
- package/lib/helper/scripts/isElementClickable.js +34 -34
- package/lib/helper.js +2 -3
- package/lib/history.js +23 -19
- package/lib/hooks.js +8 -8
- package/lib/html.js +94 -104
- package/lib/index.js +38 -27
- package/lib/listener/config.js +30 -23
- package/lib/listener/emptyRun.js +54 -0
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/exit.js +16 -18
- package/lib/listener/globalRetry.js +70 -0
- package/lib/listener/globalTimeout.js +181 -0
- package/lib/listener/helpers.js +76 -51
- package/lib/listener/mocha.js +10 -11
- package/lib/listener/result.js +11 -0
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +71 -59
- package/lib/listener/store.js +20 -0
- package/lib/locator.js +214 -197
- package/lib/mocha/asyncWrapper.js +274 -0
- package/lib/mocha/bdd.js +167 -0
- package/lib/mocha/cli.js +341 -0
- package/lib/mocha/factory.js +163 -0
- package/lib/mocha/featureConfig.js +89 -0
- package/lib/mocha/gherkin.js +231 -0
- package/lib/mocha/hooks.js +121 -0
- package/lib/mocha/index.js +21 -0
- package/lib/mocha/inject.js +46 -0
- package/lib/{interfaces → mocha}/scenarioConfig.js +58 -34
- package/lib/mocha/suite.js +89 -0
- package/lib/mocha/test.js +184 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +242 -0
- package/lib/output.js +141 -71
- package/lib/parser.js +47 -44
- package/lib/pause.js +173 -145
- package/lib/plugin/analyze.js +403 -0
- package/lib/plugin/{autoLogin.js → auth.js} +178 -79
- package/lib/plugin/autoDelay.js +36 -40
- package/lib/plugin/coverage.js +131 -78
- package/lib/plugin/customLocator.js +22 -21
- package/lib/plugin/customReporter.js +53 -0
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/heal.js +101 -110
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/pauseOnFail.js +12 -11
- package/lib/plugin/retryFailedStep.js +82 -47
- package/lib/plugin/screenshotOnFail.js +111 -92
- package/lib/plugin/stepByStepReport.js +159 -101
- package/lib/plugin/stepTimeout.js +20 -25
- package/lib/plugin/subtitles.js +38 -38
- package/lib/recorder.js +193 -130
- package/lib/rerun.js +94 -49
- package/lib/result.js +238 -0
- package/lib/retryCoordinator.js +207 -0
- package/lib/secret.js +20 -18
- package/lib/session.js +95 -89
- package/lib/step/base.js +239 -0
- package/lib/step/comment.js +10 -0
- package/lib/step/config.js +50 -0
- package/lib/step/func.js +46 -0
- package/lib/step/helper.js +50 -0
- package/lib/step/meta.js +99 -0
- package/lib/step/record.js +74 -0
- package/lib/step/retry.js +11 -0
- package/lib/step/section.js +55 -0
- package/lib/step.js +18 -329
- package/lib/steps.js +54 -0
- package/lib/store.js +38 -7
- package/lib/template/heal.js +3 -12
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +334 -0
- package/lib/timeout.js +60 -0
- package/lib/transform.js +8 -8
- package/lib/translation.js +34 -21
- package/lib/utils/loaderCheck.js +124 -0
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils/typescript.js +237 -0
- package/lib/utils.js +411 -228
- package/lib/workerStorage.js +37 -34
- package/lib/workers.js +532 -296
- package/package.json +124 -95
- package/translations/de-DE.js +5 -3
- package/translations/fr-FR.js +5 -4
- package/translations/index.js +22 -12
- package/translations/it-IT.js +4 -3
- package/translations/ja-JP.js +4 -3
- package/translations/nl-NL.js +76 -0
- package/translations/pl-PL.js +4 -3
- package/translations/pt-BR.js +4 -3
- package/translations/ru-RU.js +4 -3
- package/translations/utils.js +10 -0
- package/translations/zh-CN.js +4 -3
- package/translations/zh-TW.js +4 -3
- package/typings/index.d.ts +546 -185
- package/typings/promiseBasedTypes.d.ts +150 -875
- package/typings/types.d.ts +547 -992
- package/lib/cli.js +0 -249
- package/lib/dirname.js +0 -5
- package/lib/helper/Expect.js +0 -425
- package/lib/helper/ExpectHelper.js +0 -399
- package/lib/helper/MockServer.js +0 -223
- package/lib/helper/Nightmare.js +0 -1411
- package/lib/helper/Protractor.js +0 -1835
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/helper/TestCafe.js +0 -1410
- package/lib/helper/clientscripts/nightmare.js +0 -213
- package/lib/helper/testcafe/testControllerHolder.js +0 -42
- package/lib/helper/testcafe/testcafe-utils.js +0 -63
- package/lib/interfaces/bdd.js +0 -98
- package/lib/interfaces/featureConfig.js +0 -69
- package/lib/interfaces/gherkin.js +0 -195
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/retry.js +0 -68
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -110
- package/lib/plugin/allure.js +0 -15
- package/lib/plugin/commentStep.js +0 -136
- package/lib/plugin/debugErrors.js +0 -67
- package/lib/plugin/eachElement.js +0 -127
- package/lib/plugin/fakerTransform.js +0 -49
- package/lib/plugin/retryTo.js +0 -121
- package/lib/plugin/selenoid.js +0 -371
- package/lib/plugin/standardActingHelpers.js +0 -9
- package/lib/plugin/tryTo.js +0 -105
- package/lib/plugin/wdio.js +0 -246
- package/lib/scenario.js +0 -222
- package/lib/ui.js +0 -238
- package/lib/within.js +0 -70
|
@@ -0,0 +1,3648 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// TypeScript: Import Node.js types for process, fs, path, etc.
|
|
3
|
+
/// <reference types="node" />
|
|
4
|
+
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import { mkdirp } from 'mkdirp'
|
|
8
|
+
import crypto from 'crypto'
|
|
9
|
+
import { threadId } from 'worker_threads'
|
|
10
|
+
import { template } from '../utils.js'
|
|
11
|
+
import { getMachineInfo } from '../command/info.js'
|
|
12
|
+
|
|
13
|
+
import event from '../event.js'
|
|
14
|
+
import output from '../output.js'
|
|
15
|
+
import Codecept from '../codecept.js'
|
|
16
|
+
|
|
17
|
+
const defaultConfig = {
|
|
18
|
+
output: typeof global !== 'undefined' && global.output_dir ? global.output_dir : './output',
|
|
19
|
+
reportFileName: 'report.html',
|
|
20
|
+
includeArtifacts: true,
|
|
21
|
+
showSteps: true,
|
|
22
|
+
showSkipped: true,
|
|
23
|
+
showMetadata: true,
|
|
24
|
+
showTags: true,
|
|
25
|
+
showRetries: true,
|
|
26
|
+
exportStats: false,
|
|
27
|
+
exportStatsPath: './stats.json',
|
|
28
|
+
keepHistory: false,
|
|
29
|
+
historyPath: './test-history.json',
|
|
30
|
+
maxHistoryEntries: 50,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* HTML Reporter Plugin for CodeceptJS
|
|
35
|
+
*
|
|
36
|
+
* Generates comprehensive HTML reports showing:
|
|
37
|
+
* - Test statistics
|
|
38
|
+
* - Feature/Scenario details
|
|
39
|
+
* - Individual step results
|
|
40
|
+
* - Test artifacts (screenshots, etc.)
|
|
41
|
+
*
|
|
42
|
+
* ## Configuration
|
|
43
|
+
*
|
|
44
|
+
* ```js
|
|
45
|
+
* "plugins": {
|
|
46
|
+
* "htmlReporter": {
|
|
47
|
+
* "enabled": true,
|
|
48
|
+
* "output": "./output",
|
|
49
|
+
* "reportFileName": "report.html",
|
|
50
|
+
* "includeArtifacts": true,
|
|
51
|
+
* "showSteps": true,
|
|
52
|
+
* "showSkipped": true,
|
|
53
|
+
* "showMetadata": true,
|
|
54
|
+
* "showTags": true,
|
|
55
|
+
* "showRetries": true,
|
|
56
|
+
* "exportStats": false,
|
|
57
|
+
* "exportStatsPath": "./stats.json",
|
|
58
|
+
* "keepHistory": false,
|
|
59
|
+
* "historyPath": "./test-history.json",
|
|
60
|
+
* "maxHistoryEntries": 50
|
|
61
|
+
* }
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export default function (config) {
|
|
66
|
+
const options = { ...defaultConfig, ...config }
|
|
67
|
+
/**
|
|
68
|
+
* TypeScript: Explicitly type reportData arrays as any[] to avoid 'never' errors
|
|
69
|
+
*/
|
|
70
|
+
let reportData = {
|
|
71
|
+
stats: {},
|
|
72
|
+
tests: [],
|
|
73
|
+
failures: [],
|
|
74
|
+
hooks: [],
|
|
75
|
+
startTime: null,
|
|
76
|
+
endTime: null,
|
|
77
|
+
retries: [],
|
|
78
|
+
config: options,
|
|
79
|
+
}
|
|
80
|
+
let currentTestSteps = []
|
|
81
|
+
let currentTestHooks = []
|
|
82
|
+
let currentBddSteps = [] // Track BDD/Gherkin steps
|
|
83
|
+
let testRetryAttempts = new Map() // Track retry attempts per test
|
|
84
|
+
let currentSuite = null // Track current suite for BDD detection
|
|
85
|
+
|
|
86
|
+
// Initialize report directory
|
|
87
|
+
const reportDir = options.output ? path.resolve(global.codecept_dir, options.output) : path.resolve(global.output_dir || './output')
|
|
88
|
+
mkdirp.sync(reportDir)
|
|
89
|
+
|
|
90
|
+
// Track overall test execution
|
|
91
|
+
event.dispatcher.on(event.all.before, () => {
|
|
92
|
+
reportData.startTime = new Date().toISOString()
|
|
93
|
+
output.print('HTML Reporter: Starting HTML report generation...')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Track test start to initialize steps and hooks collection
|
|
97
|
+
event.dispatcher.on(event.test.before, test => {
|
|
98
|
+
currentTestSteps = []
|
|
99
|
+
currentTestHooks = []
|
|
100
|
+
currentBddSteps = []
|
|
101
|
+
|
|
102
|
+
// Track current suite for BDD detection
|
|
103
|
+
currentSuite = test.parent
|
|
104
|
+
|
|
105
|
+
// Enhanced retry detection with priority-based approach
|
|
106
|
+
const testId = generateTestId(test)
|
|
107
|
+
|
|
108
|
+
// Only set retry count if not already set, using priority order
|
|
109
|
+
if (!testRetryAttempts.has(testId)) {
|
|
110
|
+
// Method 1: Check retryNum property (most reliable)
|
|
111
|
+
if (test.retryNum && test.retryNum > 0) {
|
|
112
|
+
testRetryAttempts.set(testId, test.retryNum)
|
|
113
|
+
output.debug(`HTML Reporter: Retry count detected (retryNum) for ${test.title}, attempts: ${test.retryNum}`)
|
|
114
|
+
}
|
|
115
|
+
// Method 2: Check currentRetry property
|
|
116
|
+
else if (test.currentRetry && test.currentRetry > 0) {
|
|
117
|
+
testRetryAttempts.set(testId, test.currentRetry)
|
|
118
|
+
output.debug(`HTML Reporter: Retry count detected (currentRetry) for ${test.title}, attempts: ${test.currentRetry}`)
|
|
119
|
+
}
|
|
120
|
+
// Method 3: Check if this is a retried test
|
|
121
|
+
else if (test.retriedTest && test.retriedTest()) {
|
|
122
|
+
const originalTest = test.retriedTest()
|
|
123
|
+
const originalTestId = generateTestId(originalTest)
|
|
124
|
+
if (!testRetryAttempts.has(originalTestId)) {
|
|
125
|
+
testRetryAttempts.set(originalTestId, 1) // Start with 1 retry
|
|
126
|
+
} else {
|
|
127
|
+
testRetryAttempts.set(originalTestId, testRetryAttempts.get(originalTestId) + 1)
|
|
128
|
+
}
|
|
129
|
+
output.debug(`HTML Reporter: Retry detected (retriedTest) for ${originalTest.title}, attempts: ${testRetryAttempts.get(originalTestId)}`)
|
|
130
|
+
}
|
|
131
|
+
// Method 4: Check if test has been seen before (indicating a retry)
|
|
132
|
+
else if (reportData.tests.some(t => t.id === testId)) {
|
|
133
|
+
testRetryAttempts.set(testId, 1) // First retry detected
|
|
134
|
+
output.debug(`HTML Reporter: Retry detected (duplicate test) for ${test.title}, attempts: 1`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Collect step information
|
|
140
|
+
event.dispatcher.on(event.step.started, step => {
|
|
141
|
+
step.htmlReporterStartTime = Date.now()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
event.dispatcher.on(event.step.finished, step => {
|
|
145
|
+
if (step.htmlReporterStartTime) {
|
|
146
|
+
step.htmlReporterDuration = Date.now() - step.htmlReporterStartTime
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Serialize args immediately to preserve them through worker serialization
|
|
150
|
+
let serializedArgs = []
|
|
151
|
+
if (step.args && Array.isArray(step.args)) {
|
|
152
|
+
serializedArgs = step.args.map(arg => {
|
|
153
|
+
try {
|
|
154
|
+
// Try to convert to JSON-friendly format
|
|
155
|
+
if (typeof arg === 'string') return arg
|
|
156
|
+
if (typeof arg === 'number') return arg
|
|
157
|
+
if (typeof arg === 'boolean') return arg
|
|
158
|
+
if (arg === null || arg === undefined) return arg
|
|
159
|
+
// For objects, try to serialize them
|
|
160
|
+
return JSON.parse(JSON.stringify(arg))
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// If serialization fails, convert to string
|
|
163
|
+
return String(arg)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
currentTestSteps.push({
|
|
169
|
+
name: step.name,
|
|
170
|
+
actor: step.actor,
|
|
171
|
+
args: serializedArgs,
|
|
172
|
+
status: step.failed ? 'failed' : 'success',
|
|
173
|
+
duration: step.htmlReporterDuration || step.duration || 0,
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Collect hook information
|
|
178
|
+
event.dispatcher.on(event.hook.started, hook => {
|
|
179
|
+
hook.htmlReporterStartTime = Date.now()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
event.dispatcher.on(event.hook.finished, hook => {
|
|
183
|
+
if (hook.htmlReporterStartTime) {
|
|
184
|
+
hook.duration = Date.now() - hook.htmlReporterStartTime
|
|
185
|
+
}
|
|
186
|
+
// Enhanced hook info: include type, name, location, error, and context
|
|
187
|
+
const hookInfo = {
|
|
188
|
+
title: hook.title,
|
|
189
|
+
type: hook.type || 'unknown', // before, after, beforeSuite, afterSuite
|
|
190
|
+
status: hook.err ? 'failed' : 'passed',
|
|
191
|
+
duration: hook.duration || 0,
|
|
192
|
+
error: hook.err ? hook.err.message || hook.err.toString() : null,
|
|
193
|
+
location: hook.file || hook.location || (hook.ctx && hook.ctx.test && hook.ctx.test.file) || null,
|
|
194
|
+
context: hook.ctx
|
|
195
|
+
? {
|
|
196
|
+
testTitle: hook.ctx.test?.title,
|
|
197
|
+
suiteTitle: hook.ctx.test?.parent?.title,
|
|
198
|
+
feature: hook.ctx.test?.parent?.feature?.name,
|
|
199
|
+
}
|
|
200
|
+
: null,
|
|
201
|
+
}
|
|
202
|
+
currentTestHooks.push(hookInfo)
|
|
203
|
+
reportData.hooks.push(hookInfo)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Collect BDD/Gherkin step information
|
|
207
|
+
event.dispatcher.on(event.bddStep.started, step => {
|
|
208
|
+
step.htmlReporterStartTime = Date.now()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
event.dispatcher.on(event.bddStep.finished, step => {
|
|
212
|
+
if (step.htmlReporterStartTime) {
|
|
213
|
+
step.htmlReporterDuration = Date.now() - step.htmlReporterStartTime
|
|
214
|
+
}
|
|
215
|
+
currentBddSteps.push({
|
|
216
|
+
keyword: step.actor || 'Given',
|
|
217
|
+
text: step.name,
|
|
218
|
+
status: step.failed ? 'failed' : 'success',
|
|
219
|
+
duration: step.htmlReporterDuration || step.duration || 0,
|
|
220
|
+
comment: step.comment,
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Collect skipped tests
|
|
225
|
+
event.dispatcher.on(event.test.skipped, test => {
|
|
226
|
+
const testId = generateTestId(test)
|
|
227
|
+
|
|
228
|
+
// Detect if this is a BDD/Gherkin test
|
|
229
|
+
const suite = test.parent || test.suite || currentSuite
|
|
230
|
+
const isBddTest = isBddGherkinTest(test, suite)
|
|
231
|
+
const featureInfo = isBddTest ? getBddFeatureInfo(test, suite) : null
|
|
232
|
+
|
|
233
|
+
// Extract parent/suite title
|
|
234
|
+
const parentTitle = test.parent?.title || test.suite?.title || (suite && suite.title) || null
|
|
235
|
+
const suiteTitle = test.suite?.title || (suite && suite.title) || null
|
|
236
|
+
|
|
237
|
+
const testData = {
|
|
238
|
+
...test,
|
|
239
|
+
id: testId,
|
|
240
|
+
state: 'pending', // Use 'pending' as the state for skipped tests
|
|
241
|
+
duration: 0,
|
|
242
|
+
steps: [],
|
|
243
|
+
hooks: [],
|
|
244
|
+
artifacts: [],
|
|
245
|
+
tags: test.tags || [],
|
|
246
|
+
meta: test.meta || {},
|
|
247
|
+
opts: test.opts || {},
|
|
248
|
+
notes: test.notes || [],
|
|
249
|
+
retryAttempts: 0,
|
|
250
|
+
uid: test.uid,
|
|
251
|
+
isBdd: isBddTest,
|
|
252
|
+
feature: featureInfo,
|
|
253
|
+
parentTitle: parentTitle,
|
|
254
|
+
suiteTitle: suiteTitle,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
reportData.tests.push(testData)
|
|
258
|
+
output.debug(`HTML Reporter: Added skipped test - ${test.title}`)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Collect test results
|
|
262
|
+
event.dispatcher.on(event.test.finished, test => {
|
|
263
|
+
const testId = generateTestId(test)
|
|
264
|
+
let retryAttempts = testRetryAttempts.get(testId) || 0
|
|
265
|
+
|
|
266
|
+
// Additional retry detection in test.finished event
|
|
267
|
+
// Check if this test has retry indicators we might have missed
|
|
268
|
+
if (retryAttempts === 0) {
|
|
269
|
+
if (test.retryNum && test.retryNum > 0) {
|
|
270
|
+
retryAttempts = test.retryNum
|
|
271
|
+
testRetryAttempts.set(testId, retryAttempts)
|
|
272
|
+
output.debug(`HTML Reporter: Late retry detection (retryNum) for ${test.title}, attempts: ${retryAttempts}`)
|
|
273
|
+
} else if (test.currentRetry && test.currentRetry > 0) {
|
|
274
|
+
retryAttempts = test.currentRetry
|
|
275
|
+
testRetryAttempts.set(testId, retryAttempts)
|
|
276
|
+
output.debug(`HTML Reporter: Late retry detection (currentRetry) for ${test.title}, attempts: ${retryAttempts}`)
|
|
277
|
+
} else if (test._retries && test._retries > 0) {
|
|
278
|
+
retryAttempts = test._retries
|
|
279
|
+
testRetryAttempts.set(testId, retryAttempts)
|
|
280
|
+
output.debug(`HTML Reporter: Late retry detection (_retries) for ${test.title}, attempts: ${retryAttempts}`)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Debug logging
|
|
285
|
+
output.debug(`HTML Reporter: Test finished - ${test.title}, State: ${test.state}, Retries: ${retryAttempts}`)
|
|
286
|
+
|
|
287
|
+
// Detect if this is a BDD/Gherkin test - use test.parent directly instead of currentSuite
|
|
288
|
+
const suite = test.parent || test.suite || currentSuite
|
|
289
|
+
const isBddTest = isBddGherkinTest(test, suite)
|
|
290
|
+
const steps = isBddTest ? currentBddSteps : currentTestSteps
|
|
291
|
+
const featureInfo = isBddTest ? getBddFeatureInfo(test, suite) : null
|
|
292
|
+
|
|
293
|
+
// Check if this test already exists in reportData.tests (from a previous retry)
|
|
294
|
+
const existingTestIndex = reportData.tests.findIndex(t => t.id === testId)
|
|
295
|
+
const hasFailedBefore = existingTestIndex >= 0 && reportData.tests[existingTestIndex] && reportData.tests[existingTestIndex].state === 'failed'
|
|
296
|
+
const currentlyFailed = test.state === 'failed'
|
|
297
|
+
|
|
298
|
+
// Debug artifacts collection (but don't process them yet - screenshots may not be ready)
|
|
299
|
+
output.debug(`HTML Reporter: Test ${test.title} artifacts at test.finished: ${JSON.stringify(test.artifacts)}`)
|
|
300
|
+
|
|
301
|
+
// Extract parent/suite title before serialization (for worker mode)
|
|
302
|
+
// This ensures the feature name is preserved when test data is JSON stringified
|
|
303
|
+
const parentTitle = test.parent?.title || test.suite?.title || (suite && suite.title) || null
|
|
304
|
+
const suiteTitle = test.suite?.title || (suite && suite.title) || null
|
|
305
|
+
|
|
306
|
+
const testData = {
|
|
307
|
+
...test,
|
|
308
|
+
id: testId,
|
|
309
|
+
duration: test.duration || 0,
|
|
310
|
+
steps: [...steps], // Copy the steps (BDD or regular)
|
|
311
|
+
hooks: [...currentTestHooks], // Copy the hooks
|
|
312
|
+
artifacts: test.artifacts || [], // Keep original artifacts for now
|
|
313
|
+
tags: test.tags || [],
|
|
314
|
+
meta: test.meta || {},
|
|
315
|
+
opts: test.opts || {},
|
|
316
|
+
notes: test.notes || [],
|
|
317
|
+
retryAttempts: currentlyFailed || hasFailedBefore ? retryAttempts : 0, // Only show retries for failed tests
|
|
318
|
+
uid: test.uid,
|
|
319
|
+
isBdd: isBddTest,
|
|
320
|
+
feature: featureInfo,
|
|
321
|
+
// Store parent/suite titles as simple strings for worker mode serialization
|
|
322
|
+
parentTitle: parentTitle,
|
|
323
|
+
suiteTitle: suiteTitle,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (existingTestIndex >= 0) {
|
|
327
|
+
// Update existing test with final result (including failed state)
|
|
328
|
+
if (existingTestIndex >= 0) reportData.tests[existingTestIndex] = testData
|
|
329
|
+
output.debug(`HTML Reporter: Updated existing test - ${test.title}, Final state: ${test.state}`)
|
|
330
|
+
} else {
|
|
331
|
+
// Add new test
|
|
332
|
+
reportData.tests.push(testData)
|
|
333
|
+
output.debug(`HTML Reporter: Added new test - ${test.title}, State: ${test.state}`)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Track retry information - only add if there were actual retries AND the test failed at some point
|
|
337
|
+
const existingRetryIndex = reportData.retries.findIndex(r => r.testId === testId)
|
|
338
|
+
|
|
339
|
+
// Only track retries if:
|
|
340
|
+
// 1. There are retry attempts detected AND (test failed now OR failed before)
|
|
341
|
+
// 2. OR there's an existing retry record (meaning it failed before)
|
|
342
|
+
if ((retryAttempts > 0 && (currentlyFailed || hasFailedBefore)) || existingRetryIndex >= 0) {
|
|
343
|
+
// If no retry attempts detected but we have an existing retry record, increment it
|
|
344
|
+
if (retryAttempts === 0 && existingRetryIndex >= 0) {
|
|
345
|
+
retryAttempts = reportData.retries[existingRetryIndex].attempts + 1
|
|
346
|
+
testRetryAttempts.set(testId, retryAttempts)
|
|
347
|
+
output.debug(`HTML Reporter: Incremented retry count for duplicate test ${test.title}, attempts: ${retryAttempts}`)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Remove existing retry info for this test and add updated one
|
|
351
|
+
reportData.retries = reportData.retries.filter(r => r.testId !== testId)
|
|
352
|
+
reportData.retries.push({
|
|
353
|
+
testId: testId,
|
|
354
|
+
testTitle: test.title,
|
|
355
|
+
attempts: retryAttempts,
|
|
356
|
+
finalState: test.state,
|
|
357
|
+
duration: test.duration || 0,
|
|
358
|
+
})
|
|
359
|
+
output.debug(`HTML Reporter: Added retry info for ${test.title}, attempts: ${retryAttempts}, state: ${test.state}`)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Fallback: If this test already exists and either failed before or is failing now, it's a retry
|
|
363
|
+
else if (existingTestIndex >= 0 && (hasFailedBefore || currentlyFailed)) {
|
|
364
|
+
const fallbackAttempts = 1
|
|
365
|
+
testRetryAttempts.set(testId, fallbackAttempts)
|
|
366
|
+
reportData.retries.push({
|
|
367
|
+
testId: testId,
|
|
368
|
+
testTitle: test.title,
|
|
369
|
+
attempts: fallbackAttempts,
|
|
370
|
+
finalState: test.state,
|
|
371
|
+
duration: test.duration || 0,
|
|
372
|
+
})
|
|
373
|
+
output.debug(`HTML Reporter: Fallback retry detection for failed test ${test.title}, attempts: ${fallbackAttempts}`)
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
// Generate final report
|
|
378
|
+
event.dispatcher.on(event.all.result, async result => {
|
|
379
|
+
reportData.endTime = new Date().toISOString()
|
|
380
|
+
reportData.duration = new Date(reportData.endTime).getTime() - new Date(reportData.startTime).getTime()
|
|
381
|
+
|
|
382
|
+
// Process artifacts now that all async tasks (including screenshots) are complete
|
|
383
|
+
output.debug(`HTML Reporter: Processing artifacts for ${reportData.tests.length} tests after all async tasks complete`)
|
|
384
|
+
|
|
385
|
+
reportData.tests.forEach(test => {
|
|
386
|
+
const originalArtifacts = test.artifacts
|
|
387
|
+
let collectedArtifacts = []
|
|
388
|
+
|
|
389
|
+
output.debug(`HTML Reporter: Processing test "${test.title}" (ID: ${test.id})`)
|
|
390
|
+
output.debug(`HTML Reporter: Test ${test.title} final artifacts: ${JSON.stringify(originalArtifacts)}`)
|
|
391
|
+
|
|
392
|
+
if (originalArtifacts) {
|
|
393
|
+
if (Array.isArray(originalArtifacts)) {
|
|
394
|
+
collectedArtifacts = originalArtifacts
|
|
395
|
+
output.debug(`HTML Reporter: Using array artifacts: ${collectedArtifacts.length} items`)
|
|
396
|
+
} else if (typeof originalArtifacts === 'object') {
|
|
397
|
+
// Convert object properties to array (screenshotOnFail plugin format)
|
|
398
|
+
collectedArtifacts = Object.values(originalArtifacts).filter(artifact => artifact)
|
|
399
|
+
output.debug(`HTML Reporter: Converted artifacts object to array: ${collectedArtifacts.length} items`)
|
|
400
|
+
output.debug(`HTML Reporter: Converted artifacts: ${JSON.stringify(collectedArtifacts)}`)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Only use filesystem fallback if no artifacts found from screenshotOnFail plugin
|
|
405
|
+
if (collectedArtifacts.length === 0 && test.state === 'failed') {
|
|
406
|
+
output.debug(`HTML Reporter: No artifacts from plugin, trying filesystem for test "${test.title}"`)
|
|
407
|
+
collectedArtifacts = collectScreenshotsFromFilesystem(test, test.id)
|
|
408
|
+
output.debug(`HTML Reporter: Collected ${collectedArtifacts.length} screenshots from filesystem for failed test "${test.title}"`)
|
|
409
|
+
if (collectedArtifacts.length > 0) {
|
|
410
|
+
output.debug(`HTML Reporter: Filesystem screenshots for "${test.title}": ${JSON.stringify(collectedArtifacts)}`)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Update test with processed artifacts
|
|
415
|
+
test.artifacts = collectedArtifacts
|
|
416
|
+
output.debug(`HTML Reporter: Final artifacts for "${test.title}": ${JSON.stringify(test.artifacts)}`)
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
// Calculate stats from our collected test data instead of using result.stats
|
|
420
|
+
const passedTests = reportData.tests.filter(t => t.state === 'passed').length
|
|
421
|
+
const failedTests = reportData.tests.filter(t => t.state === 'failed').length
|
|
422
|
+
// Combine pending and skipped tests (both represent tests that were not run)
|
|
423
|
+
const pendingTests = reportData.tests.filter(t => t.state === 'pending' || t.state === 'skipped').length
|
|
424
|
+
|
|
425
|
+
// Calculate flaky tests (passed but had retries)
|
|
426
|
+
const flakyTests = reportData.tests.filter(t => t.state === 'passed' && t.retryAttempts > 0).length
|
|
427
|
+
|
|
428
|
+
// Count total artifacts
|
|
429
|
+
const totalArtifacts = reportData.tests.reduce((sum, t) => sum + (t.artifacts?.length || 0), 0)
|
|
430
|
+
|
|
431
|
+
// Populate failures from our collected test data with enhanced details
|
|
432
|
+
reportData.failures = reportData.tests
|
|
433
|
+
.filter(t => t.state === 'failed')
|
|
434
|
+
.map(t => {
|
|
435
|
+
const testName = t.title || 'Unknown Test'
|
|
436
|
+
// Try to get feature name from BDD, preserved titles (worker mode), or direct access
|
|
437
|
+
let featureName = t.feature?.name || t.parentTitle || t.suiteTitle || t.parent?.title || t.suite?.title || 'Unknown Feature'
|
|
438
|
+
if (featureName === 'Unknown Feature' && t.suite && t.suite.feature && t.suite.feature.name) {
|
|
439
|
+
featureName = t.suite.feature.name
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (t.err) {
|
|
443
|
+
const errorMessage = t.err.message || t.err.toString() || 'Test failed'
|
|
444
|
+
const errorStack = t.err.stack || ''
|
|
445
|
+
const filePath = t.file || t.parent?.file || ''
|
|
446
|
+
|
|
447
|
+
// Create enhanced failure object with test details
|
|
448
|
+
return {
|
|
449
|
+
testName: testName,
|
|
450
|
+
featureName: featureName,
|
|
451
|
+
message: errorMessage,
|
|
452
|
+
stack: errorStack,
|
|
453
|
+
filePath: filePath,
|
|
454
|
+
toString: () => `${testName} (${featureName})\n${errorMessage}\n${errorStack}`.trim(),
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
testName: testName,
|
|
460
|
+
featureName: featureName,
|
|
461
|
+
message: `Test failed: ${testName}`,
|
|
462
|
+
stack: '',
|
|
463
|
+
filePath: t.file || t.parent?.file || '',
|
|
464
|
+
toString: () => `${testName} (${featureName})\nTest failed: ${testName}`,
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
reportData.stats = {
|
|
469
|
+
tests: reportData.tests.length,
|
|
470
|
+
passes: passedTests,
|
|
471
|
+
failures: failedTests,
|
|
472
|
+
pending: pendingTests,
|
|
473
|
+
duration: reportData.duration,
|
|
474
|
+
failedHooks: result.stats?.failedHooks || 0,
|
|
475
|
+
flaky: flakyTests,
|
|
476
|
+
artifacts: totalArtifacts,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Debug logging for final stats
|
|
480
|
+
output.debug(`HTML Reporter: Calculated stats - Tests: ${reportData.stats.tests}, Passes: ${reportData.stats.passes}, Failures: ${reportData.stats.failures}`)
|
|
481
|
+
output.debug(`HTML Reporter: Collected ${reportData.tests.length} tests in reportData`)
|
|
482
|
+
output.debug(`HTML Reporter: Failures array has ${reportData.failures.length} items`)
|
|
483
|
+
output.debug(`HTML Reporter: Retries array has ${reportData.retries.length} items`)
|
|
484
|
+
output.debug(`HTML Reporter: testRetryAttempts Map size: ${testRetryAttempts.size}`)
|
|
485
|
+
|
|
486
|
+
// Log retry attempts map contents
|
|
487
|
+
for (const [testId, attempts] of testRetryAttempts.entries()) {
|
|
488
|
+
output.debug(`HTML Reporter: testRetryAttempts - ${testId}: ${attempts} attempts`)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
reportData.tests.forEach(test => {
|
|
492
|
+
output.debug(`HTML Reporter: Test in reportData - ${test.title}, State: ${test.state}, Retries: ${test.retryAttempts}`)
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
// Check if running with workers
|
|
496
|
+
if (process.env.RUNS_WITH_WORKERS) {
|
|
497
|
+
// In worker mode, save results to a JSON file for later consolidation
|
|
498
|
+
const workerId = threadId
|
|
499
|
+
const jsonFileName = `worker-${workerId}-results.json`
|
|
500
|
+
const jsonPath = path.join(reportDir, jsonFileName)
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
// Always overwrite the file with the latest complete data from this worker
|
|
504
|
+
// This prevents double-counting when the event is triggered multiple times
|
|
505
|
+
fs.writeFileSync(jsonPath, safeJsonStringify(reportData))
|
|
506
|
+
output.debug(`HTML Reporter: Generated worker JSON results: ${jsonFileName}`)
|
|
507
|
+
} catch (error) {
|
|
508
|
+
output.debug(`HTML Reporter: Failed to write worker JSON: ${error.message}`)
|
|
509
|
+
}
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Single process mode - generate report normally
|
|
514
|
+
try {
|
|
515
|
+
await generateHtmlReport(reportData, options)
|
|
516
|
+
} catch (error) {
|
|
517
|
+
output.print(`Failed to generate HTML report: ${error.message}`)
|
|
518
|
+
output.debug(`HTML Reporter error stack: ${error.stack}`)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Export stats if configured
|
|
522
|
+
if (options.exportStats) {
|
|
523
|
+
exportTestStats(reportData, options)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Save history if configured
|
|
527
|
+
if (options.keepHistory) {
|
|
528
|
+
saveTestHistory(reportData, options)
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
// Handle worker consolidation after all workers complete
|
|
533
|
+
event.dispatcher.on(event.workers.result, async result => {
|
|
534
|
+
if (process.env.RUNS_WITH_WORKERS) {
|
|
535
|
+
// Only run consolidation in main process
|
|
536
|
+
await consolidateWorkerJsonResults(options)
|
|
537
|
+
}
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Safely serialize data to JSON, handling circular references
|
|
542
|
+
*/
|
|
543
|
+
function safeJsonStringify(data) {
|
|
544
|
+
const seen = new WeakSet()
|
|
545
|
+
return JSON.stringify(
|
|
546
|
+
data,
|
|
547
|
+
(key, value) => {
|
|
548
|
+
if (typeof value === 'object' && value !== null) {
|
|
549
|
+
if (seen.has(value)) {
|
|
550
|
+
// For error objects, try to extract useful information instead of "[Circular Reference]"
|
|
551
|
+
if (key === 'err' || key === 'error') {
|
|
552
|
+
return {
|
|
553
|
+
message: value.message || 'Error occurred',
|
|
554
|
+
stack: value.stack || '',
|
|
555
|
+
name: value.name || 'Error',
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Skip circular references for other objects
|
|
559
|
+
return undefined
|
|
560
|
+
}
|
|
561
|
+
seen.add(value)
|
|
562
|
+
|
|
563
|
+
// Special handling for error objects to preserve important properties
|
|
564
|
+
if (value instanceof Error || (value.message && value.stack)) {
|
|
565
|
+
return {
|
|
566
|
+
message: value.message || '',
|
|
567
|
+
stack: value.stack || '',
|
|
568
|
+
name: value.name || 'Error',
|
|
569
|
+
toString: () => value.message || 'Error occurred',
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return value
|
|
574
|
+
},
|
|
575
|
+
2,
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function generateTestId(test) {
|
|
580
|
+
return crypto
|
|
581
|
+
.createHash('sha256')
|
|
582
|
+
.update(`${test.parent?.title || 'unknown'}_${test.title}`)
|
|
583
|
+
.digest('hex')
|
|
584
|
+
.substring(0, 8)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function collectScreenshotsFromFilesystem(test, testId) {
|
|
588
|
+
const screenshots = []
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
// Common screenshot locations to check
|
|
592
|
+
const possibleDirs = [
|
|
593
|
+
reportDir, // Same as report directory
|
|
594
|
+
global.output_dir || './output', // Global output directory
|
|
595
|
+
path.resolve(global.codecept_dir || '.', 'output'), // Codecept output directory
|
|
596
|
+
path.resolve('.', 'output'), // Current directory output
|
|
597
|
+
path.resolve('.', '_output'), // Alternative output directory
|
|
598
|
+
path.resolve('output'), // Relative output directory
|
|
599
|
+
path.resolve('qa', 'output'), // QA project output directory
|
|
600
|
+
path.resolve('..', 'qa', 'output'), // Parent QA project output directory
|
|
601
|
+
]
|
|
602
|
+
|
|
603
|
+
// Use the exact same logic as screenshotOnFail plugin's testToFileName function
|
|
604
|
+
const originalTestName = test.title || 'test'
|
|
605
|
+
const originalFeatureName = test.parent?.title || 'feature'
|
|
606
|
+
|
|
607
|
+
// Replicate testToFileName logic from lib/mocha/test.js
|
|
608
|
+
function replicateTestToFileName(testTitle) {
|
|
609
|
+
let fileName = testTitle
|
|
610
|
+
|
|
611
|
+
// Slice to 100 characters first
|
|
612
|
+
fileName = fileName.slice(0, 100)
|
|
613
|
+
|
|
614
|
+
// Handle data-driven tests: remove everything from '{' onwards (with 3 chars before)
|
|
615
|
+
if (fileName.indexOf('{') !== -1) {
|
|
616
|
+
fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim()
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Apply clearString logic from utils.js
|
|
620
|
+
if (fileName.endsWith('.')) {
|
|
621
|
+
fileName = fileName.slice(0, -1)
|
|
622
|
+
}
|
|
623
|
+
fileName = fileName
|
|
624
|
+
.replace(/ /g, '_')
|
|
625
|
+
.replace(/"/g, "'")
|
|
626
|
+
.replace(/\//g, '_')
|
|
627
|
+
.replace(/</g, '(')
|
|
628
|
+
.replace(/>/g, ')')
|
|
629
|
+
.replace(/:/g, '_')
|
|
630
|
+
.replace(/\\/g, '_')
|
|
631
|
+
.replace(/\|/g, '_')
|
|
632
|
+
.replace(/\?/g, '.')
|
|
633
|
+
.replace(/\*/g, '^')
|
|
634
|
+
.replace(/'/g, '')
|
|
635
|
+
|
|
636
|
+
// Final slice to 100 characters
|
|
637
|
+
return fileName.slice(0, 100)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const testName = replicateTestToFileName(originalTestName)
|
|
641
|
+
const featureName = replicateTestToFileName(originalFeatureName)
|
|
642
|
+
|
|
643
|
+
output.debug(`HTML Reporter: Original test title: "${originalTestName}"`)
|
|
644
|
+
output.debug(`HTML Reporter: CodeceptJS filename: "${testName}"`)
|
|
645
|
+
|
|
646
|
+
// Generate possible screenshot names based on CodeceptJS patterns
|
|
647
|
+
const possibleNames = [
|
|
648
|
+
`${testName}.failed.png`, // Primary CodeceptJS screenshotOnFail pattern
|
|
649
|
+
`${testName}.failed.jpg`,
|
|
650
|
+
`${featureName}_${testName}.failed.png`,
|
|
651
|
+
`${featureName}_${testName}.failed.jpg`,
|
|
652
|
+
`Test_${testName}.failed.png`, // Alternative pattern
|
|
653
|
+
`Test_${testName}.failed.jpg`,
|
|
654
|
+
`${testName}.png`,
|
|
655
|
+
`${testName}.jpg`,
|
|
656
|
+
`${featureName}_${testName}.png`,
|
|
657
|
+
`${featureName}_${testName}.jpg`,
|
|
658
|
+
`failed_${testName}.png`,
|
|
659
|
+
`failed_${testName}.jpg`,
|
|
660
|
+
`screenshot_${testId}.png`,
|
|
661
|
+
`screenshot_${testId}.jpg`,
|
|
662
|
+
'screenshot.png',
|
|
663
|
+
'screenshot.jpg',
|
|
664
|
+
'failure.png',
|
|
665
|
+
'failure.jpg',
|
|
666
|
+
]
|
|
667
|
+
|
|
668
|
+
output.debug(`HTML Reporter: Checking ${possibleNames.length} possible screenshot names for "${testName}"`)
|
|
669
|
+
|
|
670
|
+
// Search for screenshots in possible directories
|
|
671
|
+
for (const dir of possibleDirs) {
|
|
672
|
+
output.debug(`HTML Reporter: Checking directory: ${dir}`)
|
|
673
|
+
if (!fs.existsSync(dir)) {
|
|
674
|
+
output.debug(`HTML Reporter: Directory does not exist: ${dir}`)
|
|
675
|
+
continue
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
const files = fs.readdirSync(dir)
|
|
680
|
+
output.debug(`HTML Reporter: Found ${files.length} files in ${dir}`)
|
|
681
|
+
|
|
682
|
+
// Look for exact matches first
|
|
683
|
+
for (const name of possibleNames) {
|
|
684
|
+
if (files.includes(name)) {
|
|
685
|
+
const fullPath = path.join(dir, name)
|
|
686
|
+
if (!screenshots.includes(fullPath)) {
|
|
687
|
+
screenshots.push(fullPath)
|
|
688
|
+
output.debug(`HTML Reporter: Found screenshot: ${fullPath}`)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Look for screenshot files that are specifically for this test
|
|
694
|
+
// Be more strict to avoid cross-test contamination
|
|
695
|
+
const screenshotFiles = files.filter(file => {
|
|
696
|
+
const lowerFile = file.toLowerCase()
|
|
697
|
+
const lowerTestName = testName.toLowerCase()
|
|
698
|
+
const lowerFeatureName = featureName.toLowerCase()
|
|
699
|
+
|
|
700
|
+
return (
|
|
701
|
+
file.match(/\.(png|jpg|jpeg|gif|webp|bmp)$/i) &&
|
|
702
|
+
// Exact test name matches with .failed pattern (most specific)
|
|
703
|
+
(file === `${testName}.failed.png` ||
|
|
704
|
+
file === `${testName}.failed.jpg` ||
|
|
705
|
+
file === `${featureName}_${testName}.failed.png` ||
|
|
706
|
+
file === `${featureName}_${testName}.failed.jpg` ||
|
|
707
|
+
file === `Test_${testName}.failed.png` ||
|
|
708
|
+
file === `Test_${testName}.failed.jpg` ||
|
|
709
|
+
// Word boundary checks for .failed pattern
|
|
710
|
+
(lowerFile.includes('.failed.') &&
|
|
711
|
+
(lowerFile.startsWith(lowerTestName + '.') || lowerFile.startsWith(lowerFeatureName + '_' + lowerTestName + '.') || lowerFile.startsWith('test_' + lowerTestName + '.'))))
|
|
712
|
+
)
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
for (const file of screenshotFiles) {
|
|
716
|
+
const fullPath = path.join(dir, file)
|
|
717
|
+
if (!screenshots.includes(fullPath)) {
|
|
718
|
+
screenshots.push(fullPath)
|
|
719
|
+
output.debug(`HTML Reporter: Found related screenshot: ${fullPath}`)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
} catch (error) {
|
|
723
|
+
// Ignore directory read errors
|
|
724
|
+
output.debug(`HTML Reporter: Could not read directory ${dir}: ${error.message}`)
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
} catch (error) {
|
|
728
|
+
output.debug(`HTML Reporter: Error collecting screenshots: ${error.message}`)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return screenshots
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function isBddGherkinTest(test, suite) {
|
|
735
|
+
// Check if the suite has BDD/Gherkin properties
|
|
736
|
+
return !!(suite && (suite.feature || suite.file?.endsWith('.feature')))
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function getBddFeatureInfo(test, suite) {
|
|
740
|
+
if (!suite) return null
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
name: suite.feature?.name || suite.title,
|
|
744
|
+
description: suite.feature?.description || suite.comment || '',
|
|
745
|
+
language: suite.feature?.language || 'en',
|
|
746
|
+
tags: suite.tags || [],
|
|
747
|
+
file: suite.file || '',
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function exportTestStats(data, config) {
|
|
752
|
+
const statsPath = path.resolve(reportDir, config.exportStatsPath)
|
|
753
|
+
|
|
754
|
+
const exportData = {
|
|
755
|
+
timestamp: data.endTime, // Already an ISO string
|
|
756
|
+
duration: data.duration,
|
|
757
|
+
stats: data.stats,
|
|
758
|
+
retries: data.retries,
|
|
759
|
+
testCount: data.tests.length,
|
|
760
|
+
passedTests: data.tests.filter(t => t.state === 'passed').length,
|
|
761
|
+
failedTests: data.tests.filter(t => t.state === 'failed').length,
|
|
762
|
+
pendingTests: data.tests.filter(t => t.state === 'pending').length,
|
|
763
|
+
tests: data.tests.map(test => ({
|
|
764
|
+
id: test.id,
|
|
765
|
+
title: test.title,
|
|
766
|
+
feature: test.parent?.title || 'Unknown',
|
|
767
|
+
state: test.state,
|
|
768
|
+
duration: test.duration,
|
|
769
|
+
tags: test.tags,
|
|
770
|
+
meta: test.meta,
|
|
771
|
+
retryAttempts: test.retryAttempts,
|
|
772
|
+
uid: test.uid,
|
|
773
|
+
})),
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
fs.writeFileSync(statsPath, JSON.stringify(exportData, null, 2))
|
|
778
|
+
output.print(`Test stats exported to: ${statsPath}`)
|
|
779
|
+
} catch (error) {
|
|
780
|
+
output.print(`Failed to export test stats: ${error.message}`)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function saveTestHistory(data, config) {
|
|
785
|
+
const historyPath = path.resolve(reportDir, config.historyPath)
|
|
786
|
+
let history = []
|
|
787
|
+
|
|
788
|
+
// Load existing history
|
|
789
|
+
try {
|
|
790
|
+
if (fs.existsSync(historyPath)) {
|
|
791
|
+
history = JSON.parse(fs.readFileSync(historyPath, 'utf8'))
|
|
792
|
+
}
|
|
793
|
+
} catch (error) {
|
|
794
|
+
output.print(`Failed to load existing history: ${error.message}`)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Add current run to history
|
|
798
|
+
history.unshift({
|
|
799
|
+
timestamp: data.endTime, // Already an ISO string
|
|
800
|
+
duration: data.duration,
|
|
801
|
+
stats: data.stats,
|
|
802
|
+
retries: data.retries.length,
|
|
803
|
+
testCount: data.tests.length,
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
// Limit history entries
|
|
807
|
+
if (history.length > config.maxHistoryEntries) {
|
|
808
|
+
history = history.slice(0, config.maxHistoryEntries)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2))
|
|
813
|
+
output.print(`Test history saved to: ${historyPath}`)
|
|
814
|
+
} catch (error) {
|
|
815
|
+
output.print(`Failed to save test history: ${error.message}`)
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Consolidates JSON reports from multiple workers into a single HTML report
|
|
821
|
+
*/
|
|
822
|
+
async function consolidateWorkerJsonResults(config) {
|
|
823
|
+
const jsonFiles = fs.readdirSync(reportDir).filter(file => file.startsWith('worker-') && file.endsWith('-results.json'))
|
|
824
|
+
|
|
825
|
+
if (jsonFiles.length === 0) {
|
|
826
|
+
output.debug('HTML Reporter: No worker JSON results found to consolidate')
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
output.debug(`HTML Reporter: Found ${jsonFiles.length} worker JSON files to consolidate`)
|
|
831
|
+
|
|
832
|
+
// Initialize consolidated data structure
|
|
833
|
+
const consolidatedData = {
|
|
834
|
+
stats: {
|
|
835
|
+
tests: 0,
|
|
836
|
+
passes: 0,
|
|
837
|
+
failures: 0,
|
|
838
|
+
pending: 0,
|
|
839
|
+
skipped: 0,
|
|
840
|
+
duration: 0,
|
|
841
|
+
failedHooks: 0,
|
|
842
|
+
},
|
|
843
|
+
tests: [],
|
|
844
|
+
failures: [],
|
|
845
|
+
hooks: [],
|
|
846
|
+
startTime: new Date(),
|
|
847
|
+
endTime: new Date(),
|
|
848
|
+
retries: [],
|
|
849
|
+
duration: 0,
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
try {
|
|
853
|
+
// Process each worker's JSON file
|
|
854
|
+
for (const jsonFile of jsonFiles) {
|
|
855
|
+
const jsonPath = path.join(reportDir, jsonFile)
|
|
856
|
+
try {
|
|
857
|
+
const workerData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
|
|
858
|
+
|
|
859
|
+
// Extract worker ID from filename (e.g., "worker-0-results.json" -> 0)
|
|
860
|
+
const workerIdMatch = jsonFile.match(/worker-(\d+)-results\.json/)
|
|
861
|
+
const workerIndex = workerIdMatch ? parseInt(workerIdMatch[1], 10) : undefined
|
|
862
|
+
|
|
863
|
+
// Merge stats
|
|
864
|
+
if (workerData.stats) {
|
|
865
|
+
consolidatedData.stats.passes += workerData.stats.passes || 0
|
|
866
|
+
consolidatedData.stats.failures += workerData.stats.failures || 0
|
|
867
|
+
consolidatedData.stats.tests += workerData.stats.tests || 0
|
|
868
|
+
consolidatedData.stats.pending += workerData.stats.pending || 0
|
|
869
|
+
consolidatedData.stats.skipped += workerData.stats.skipped || 0
|
|
870
|
+
consolidatedData.stats.duration += workerData.stats.duration || 0
|
|
871
|
+
consolidatedData.stats.failedHooks += workerData.stats.failedHooks || 0
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Merge tests and add worker index
|
|
875
|
+
if (workerData.tests) {
|
|
876
|
+
const testsWithWorkerIndex = workerData.tests.map(test => ({
|
|
877
|
+
...test,
|
|
878
|
+
workerIndex: workerIndex,
|
|
879
|
+
}))
|
|
880
|
+
consolidatedData.tests.push(...testsWithWorkerIndex)
|
|
881
|
+
}
|
|
882
|
+
if (workerData.failures) consolidatedData.failures.push(...workerData.failures)
|
|
883
|
+
if (workerData.hooks) consolidatedData.hooks.push(...workerData.hooks)
|
|
884
|
+
if (workerData.retries) consolidatedData.retries.push(...workerData.retries)
|
|
885
|
+
|
|
886
|
+
// Update timestamps
|
|
887
|
+
if (workerData.startTime) {
|
|
888
|
+
const workerStart = new Date(workerData.startTime).getTime()
|
|
889
|
+
const currentStart = new Date(consolidatedData.startTime).getTime()
|
|
890
|
+
if (workerStart < currentStart) {
|
|
891
|
+
consolidatedData.startTime = workerData.startTime
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (workerData.endTime) {
|
|
896
|
+
const workerEnd = new Date(workerData.endTime).getTime()
|
|
897
|
+
const currentEnd = new Date(consolidatedData.endTime).getTime()
|
|
898
|
+
if (workerEnd > currentEnd) {
|
|
899
|
+
consolidatedData.endTime = workerData.endTime
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Update duration
|
|
904
|
+
if (workerData.duration) {
|
|
905
|
+
consolidatedData.duration = Math.max(consolidatedData.duration, workerData.duration)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Clean up the worker JSON file
|
|
909
|
+
try {
|
|
910
|
+
fs.unlinkSync(jsonPath)
|
|
911
|
+
} catch (error) {
|
|
912
|
+
output.print(`Failed to delete worker JSON file ${jsonFile}: ${error.message}`)
|
|
913
|
+
}
|
|
914
|
+
} catch (error) {
|
|
915
|
+
output.print(`Failed to process worker JSON file ${jsonFile}: ${error.message}`)
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Generate the final HTML report
|
|
920
|
+
generateHtmlReport(consolidatedData, config)
|
|
921
|
+
|
|
922
|
+
// Export stats if configured
|
|
923
|
+
if (config.exportStats) {
|
|
924
|
+
exportTestStats(consolidatedData, config)
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Save history if configured
|
|
928
|
+
if (config.keepHistory) {
|
|
929
|
+
saveTestHistory(consolidatedData, config)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
output.debug(`HTML Reporter: Successfully consolidated ${jsonFiles.length} worker reports`)
|
|
933
|
+
} catch (error) {
|
|
934
|
+
output.debug(`HTML Reporter: Failed to consolidate worker reports: ${error.message}`)
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function generateHtmlReport(data, config) {
|
|
939
|
+
const reportPath = path.join(reportDir, config.reportFileName)
|
|
940
|
+
|
|
941
|
+
// Load history if available
|
|
942
|
+
let history = []
|
|
943
|
+
if (config.keepHistory) {
|
|
944
|
+
const historyPath = path.resolve(reportDir, config.historyPath)
|
|
945
|
+
try {
|
|
946
|
+
if (fs.existsSync(historyPath)) {
|
|
947
|
+
history = JSON.parse(fs.readFileSync(historyPath, 'utf8')) // Show all available history
|
|
948
|
+
}
|
|
949
|
+
} catch (error) {
|
|
950
|
+
output.print(`Failed to load history for report: ${error.message}`)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Add current run to history for chart display (before saving to file)
|
|
954
|
+
const currentRun = {
|
|
955
|
+
timestamp: data.endTime, // Already an ISO string
|
|
956
|
+
duration: data.duration,
|
|
957
|
+
stats: data.stats,
|
|
958
|
+
retries: data.retries.length,
|
|
959
|
+
testCount: data.tests.length,
|
|
960
|
+
}
|
|
961
|
+
history.unshift(currentRun)
|
|
962
|
+
|
|
963
|
+
// Limit history entries for chart display
|
|
964
|
+
if (history.length > config.maxHistoryEntries) {
|
|
965
|
+
history = history.slice(0, config.maxHistoryEntries)
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Get system information
|
|
970
|
+
const systemInfo = await getMachineInfo()
|
|
971
|
+
|
|
972
|
+
const html = template(getHtmlTemplate(), {
|
|
973
|
+
title: `CodeceptJS Test Report v${Codecept.version()}`,
|
|
974
|
+
timestamp: data.endTime, // Already an ISO string
|
|
975
|
+
duration: formatDuration(data.duration),
|
|
976
|
+
stats: JSON.stringify(data.stats),
|
|
977
|
+
history: JSON.stringify(history),
|
|
978
|
+
statsHtml: generateStatsHtml(data.stats),
|
|
979
|
+
testsHtml: generateTestsHtml(data.tests, config),
|
|
980
|
+
retriesHtml: config.showRetries ? generateRetriesHtml(data.retries) : '',
|
|
981
|
+
cssStyles: getCssStyles(),
|
|
982
|
+
jsScripts: getJsScripts(),
|
|
983
|
+
showRetries: config.showRetries ? 'block' : 'none',
|
|
984
|
+
showHistory: config.keepHistory && history.length > 0 ? 'block' : 'none',
|
|
985
|
+
codeceptVersion: Codecept.version(),
|
|
986
|
+
systemInfoHtml: generateSystemInfoHtml(systemInfo),
|
|
987
|
+
})
|
|
988
|
+
|
|
989
|
+
fs.writeFileSync(reportPath, html)
|
|
990
|
+
output.print(`HTML Report saved to: ${reportPath}`)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function generateStatsHtml(stats) {
|
|
994
|
+
const passed = stats.passes || 0
|
|
995
|
+
const failed = stats.failures || 0
|
|
996
|
+
const pending = stats.pending || 0
|
|
997
|
+
const total = stats.tests || 0
|
|
998
|
+
const flaky = stats.flaky || 0
|
|
999
|
+
const artifactCount = stats.artifacts || 0
|
|
1000
|
+
const passRate = total > 0 ? ((passed / total) * 100).toFixed(1) : '0.0'
|
|
1001
|
+
const failRate = total > 0 ? ((failed / total) * 100).toFixed(1) : '0.0'
|
|
1002
|
+
|
|
1003
|
+
return `
|
|
1004
|
+
<div class="stats-cards">
|
|
1005
|
+
<div class="stat-card total">
|
|
1006
|
+
<h3>Total</h3>
|
|
1007
|
+
<span class="stat-number">${total}</span>
|
|
1008
|
+
</div>
|
|
1009
|
+
<div class="stat-card passed">
|
|
1010
|
+
<h3>Passed</h3>
|
|
1011
|
+
<span class="stat-number">${passed}</span>
|
|
1012
|
+
</div>
|
|
1013
|
+
<div class="stat-card failed">
|
|
1014
|
+
<h3>Failed</h3>
|
|
1015
|
+
<span class="stat-number">${failed}</span>
|
|
1016
|
+
</div>
|
|
1017
|
+
<div class="stat-card pending">
|
|
1018
|
+
<h3>Skipped</h3>
|
|
1019
|
+
<span class="stat-number">${pending}</span>
|
|
1020
|
+
</div>
|
|
1021
|
+
<div class="stat-card flaky">
|
|
1022
|
+
<h3>Flaky</h3>
|
|
1023
|
+
<span class="stat-number">${flaky}</span>
|
|
1024
|
+
</div>
|
|
1025
|
+
<div class="stat-card artifacts">
|
|
1026
|
+
<h3>Artifacts</h3>
|
|
1027
|
+
<span class="stat-number">${artifactCount}</span>
|
|
1028
|
+
</div>
|
|
1029
|
+
</div>
|
|
1030
|
+
<div class="metrics-summary">
|
|
1031
|
+
<span>Pass Rate: <strong>${passRate}%</strong></span>
|
|
1032
|
+
<span>Fail Rate: <strong>${failRate}%</strong></span>
|
|
1033
|
+
</div>
|
|
1034
|
+
<div class="pie-chart-container">
|
|
1035
|
+
<canvas id="statsChart" width="300" height="300"></canvas>
|
|
1036
|
+
<script>
|
|
1037
|
+
// Pie chart data will be rendered by JavaScript
|
|
1038
|
+
window.chartData = {
|
|
1039
|
+
passed: ${passed},
|
|
1040
|
+
failed: ${failed},
|
|
1041
|
+
pending: ${pending}
|
|
1042
|
+
};
|
|
1043
|
+
</script>
|
|
1044
|
+
</div>
|
|
1045
|
+
`
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function generateTestsHtml(tests, config) {
|
|
1049
|
+
if (!tests || tests.length === 0) {
|
|
1050
|
+
return '<p>No tests found.</p>'
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Group tests by feature name
|
|
1054
|
+
const grouped = {}
|
|
1055
|
+
tests.forEach(test => {
|
|
1056
|
+
const feature = test.isBdd && test.feature ? test.feature.name : test.parentTitle || test.suiteTitle || test.parent?.title || test.suite?.title || 'Unknown Feature'
|
|
1057
|
+
if (!grouped[feature]) grouped[feature] = []
|
|
1058
|
+
grouped[feature].push(test)
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
// Render each feature section
|
|
1062
|
+
return Object.entries(grouped)
|
|
1063
|
+
.map(([feature, tests]) => {
|
|
1064
|
+
const featureId = feature.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()
|
|
1065
|
+
return `
|
|
1066
|
+
<section class="feature-group">
|
|
1067
|
+
<h3 class="feature-group-title" onclick="toggleFeatureGroup('${featureId}')">
|
|
1068
|
+
${escapeHtml(feature)}
|
|
1069
|
+
<span class="toggle-icon">▼</span>
|
|
1070
|
+
</h3>
|
|
1071
|
+
<div class="feature-tests" id="feature-${featureId}">
|
|
1072
|
+
${tests
|
|
1073
|
+
.map(test => {
|
|
1074
|
+
const statusClass = test.state || 'unknown'
|
|
1075
|
+
const steps = config.showSteps && test.steps ? (test.isBdd ? generateBddStepsHtml(test.steps) : generateStepsHtml(test.steps)) : ''
|
|
1076
|
+
const featureDetails = test.isBdd && test.feature ? generateBddFeatureHtml(test.feature) : ''
|
|
1077
|
+
const hooks = test.hooks && test.hooks.length > 0 ? generateHooksHtml(test.hooks) : ''
|
|
1078
|
+
const artifacts = config.includeArtifacts && test.artifacts ? generateArtifactsHtml(test.artifacts, test.state === 'failed') : ''
|
|
1079
|
+
const metadata = config.showMetadata && (test.meta || test.opts) ? generateMetadataHtml(test.meta, test.opts) : ''
|
|
1080
|
+
const tags = config.showTags && test.tags && test.tags.length > 0 ? generateTagsHtml(test.tags) : ''
|
|
1081
|
+
const retries = config.showRetries && test.retryAttempts > 0 ? generateTestRetryHtml(test.retryAttempts, test.state) : ''
|
|
1082
|
+
const notes = test.notes && test.notes.length > 0 ? generateNotesHtml(test.notes) : ''
|
|
1083
|
+
|
|
1084
|
+
// Worker badge - show worker index if test has worker info
|
|
1085
|
+
const workerBadge = test.workerIndex !== undefined ? `<span class="worker-badge worker-${test.workerIndex}">Worker ${test.workerIndex}</span>` : ''
|
|
1086
|
+
|
|
1087
|
+
return `
|
|
1088
|
+
<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'}">
|
|
1089
|
+
<div class="test-header" onclick="toggleTestDetails('test-${test.id}')">
|
|
1090
|
+
<span class="test-status ${statusClass}">●</span>
|
|
1091
|
+
<div class="test-info">
|
|
1092
|
+
<h3 class="test-title">${test.isBdd ? `Scenario: ${test.title}` : test.title}</h3>
|
|
1093
|
+
<div class="test-meta-line">
|
|
1094
|
+
${workerBadge}
|
|
1095
|
+
${test.uid ? `<span class="test-uid">${test.uid}</span>` : ''}
|
|
1096
|
+
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
1097
|
+
${test.retryAttempts > 0 ? `<span class="retry-badge">${test.retryAttempts} retries</span>` : ''}
|
|
1098
|
+
${test.isBdd ? '<span class="bdd-badge">Gherkin</span>' : ''}
|
|
1099
|
+
</div>
|
|
1100
|
+
</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
<div class="test-details" id="details-test-${test.id}">
|
|
1103
|
+
${test.err ? `<div class="error-message"><pre>${escapeHtml(getErrorMessage(test))}</pre></div>` : ''}
|
|
1104
|
+
${featureDetails}
|
|
1105
|
+
${tags}
|
|
1106
|
+
${metadata}
|
|
1107
|
+
${retries}
|
|
1108
|
+
${notes}
|
|
1109
|
+
${hooks}
|
|
1110
|
+
${steps}
|
|
1111
|
+
${artifacts}
|
|
1112
|
+
</div>
|
|
1113
|
+
</div>
|
|
1114
|
+
`
|
|
1115
|
+
})
|
|
1116
|
+
.join('')}
|
|
1117
|
+
</div>
|
|
1118
|
+
</section>
|
|
1119
|
+
`
|
|
1120
|
+
})
|
|
1121
|
+
.join('')
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function generateStepsHtml(steps) {
|
|
1125
|
+
if (!steps || steps.length === 0) return ''
|
|
1126
|
+
|
|
1127
|
+
const stepsHtml = steps
|
|
1128
|
+
.map(step => {
|
|
1129
|
+
const statusClass = step.status || 'unknown'
|
|
1130
|
+
const args = step.args ? step.args.map(arg => JSON.stringify(arg)).join(', ') : ''
|
|
1131
|
+
const stepName = step.name || 'unknown step'
|
|
1132
|
+
const actor = step.actor || 'I'
|
|
1133
|
+
|
|
1134
|
+
return `
|
|
1135
|
+
<div class="step-item ${statusClass}">
|
|
1136
|
+
<span class="step-status ${statusClass}">●</span>
|
|
1137
|
+
<span class="step-title">${actor}.${stepName}(${args})</span>
|
|
1138
|
+
<span class="step-duration">${formatDuration(step.duration)}</span>
|
|
1139
|
+
</div>
|
|
1140
|
+
`
|
|
1141
|
+
})
|
|
1142
|
+
.join('')
|
|
1143
|
+
|
|
1144
|
+
return `
|
|
1145
|
+
<div class="steps-section">
|
|
1146
|
+
<h4>Steps:</h4>
|
|
1147
|
+
<div class="steps-list">${stepsHtml}</div>
|
|
1148
|
+
</div>
|
|
1149
|
+
`
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function generateBddStepsHtml(steps) {
|
|
1153
|
+
if (!steps || steps.length === 0) return ''
|
|
1154
|
+
|
|
1155
|
+
const stepsHtml = steps
|
|
1156
|
+
.map(step => {
|
|
1157
|
+
const statusClass = step.status || 'unknown'
|
|
1158
|
+
const keyword = step.keyword || 'Given'
|
|
1159
|
+
const text = step.text || ''
|
|
1160
|
+
const comment = step.comment ? `<div class="step-comment">${escapeHtml(step.comment)}</div>` : ''
|
|
1161
|
+
|
|
1162
|
+
return `
|
|
1163
|
+
<div class="bdd-step-item ${statusClass}">
|
|
1164
|
+
<span class="step-status ${statusClass}">●</span>
|
|
1165
|
+
<span class="bdd-keyword">${keyword}</span>
|
|
1166
|
+
<span class="bdd-step-text">${escapeHtml(text)}</span>
|
|
1167
|
+
<span class="step-duration">${formatDuration(step.duration)}</span>
|
|
1168
|
+
${comment}
|
|
1169
|
+
</div>
|
|
1170
|
+
`
|
|
1171
|
+
})
|
|
1172
|
+
.join('')
|
|
1173
|
+
|
|
1174
|
+
return `
|
|
1175
|
+
<div class="bdd-steps-section">
|
|
1176
|
+
<h4>Scenario Steps:</h4>
|
|
1177
|
+
<div class="bdd-steps-list">${stepsHtml}</div>
|
|
1178
|
+
</div>
|
|
1179
|
+
`
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function generateBddFeatureHtml(feature) {
|
|
1183
|
+
if (!feature) return ''
|
|
1184
|
+
|
|
1185
|
+
const description = feature.description ? `<div class="feature-description">${escapeHtml(feature.description)}</div>` : ''
|
|
1186
|
+
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>` : ''
|
|
1187
|
+
|
|
1188
|
+
return `
|
|
1189
|
+
<div class="bdd-feature-section">
|
|
1190
|
+
<h4>Feature Information:</h4>
|
|
1191
|
+
<div class="feature-info">
|
|
1192
|
+
<div class="feature-name">Feature: ${escapeHtml(feature.name)}</div>
|
|
1193
|
+
${description}
|
|
1194
|
+
${featureTags}
|
|
1195
|
+
${feature.file ? `<div class="feature-file">File: ${escapeHtml(feature.file)}</div>` : ''}
|
|
1196
|
+
</div>
|
|
1197
|
+
</div>
|
|
1198
|
+
`
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function generateHooksHtml(hooks) {
|
|
1202
|
+
if (!hooks || hooks.length === 0) return ''
|
|
1203
|
+
|
|
1204
|
+
const hooksHtml = hooks
|
|
1205
|
+
.map(hook => {
|
|
1206
|
+
const statusClass = hook.status || 'unknown'
|
|
1207
|
+
const hookType = hook.type || 'hook'
|
|
1208
|
+
const hookTitle = hook.title || `${hookType} hook`
|
|
1209
|
+
const location = hook.location ? `<div class="hook-location">Location: ${escapeHtml(hook.location)}</div>` : ''
|
|
1210
|
+
const context = hook.context ? `<div class="hook-context">Test: ${escapeHtml(hook.context.testTitle || 'N/A')}, Suite: ${escapeHtml(hook.context.suiteTitle || 'N/A')}</div>` : ''
|
|
1211
|
+
|
|
1212
|
+
return `
|
|
1213
|
+
<div class="hook-item ${statusClass}">
|
|
1214
|
+
<span class="hook-status ${statusClass}">●</span>
|
|
1215
|
+
<div class="hook-content">
|
|
1216
|
+
<span class="hook-title">${hookType}: ${hookTitle}</span>
|
|
1217
|
+
<span class="hook-duration">${formatDuration(hook.duration)}</span>
|
|
1218
|
+
${location}
|
|
1219
|
+
${context}
|
|
1220
|
+
${hook.error ? `<div class="hook-error">${escapeHtml(hook.error)}</div>` : ''}
|
|
1221
|
+
</div>
|
|
1222
|
+
</div>
|
|
1223
|
+
`
|
|
1224
|
+
})
|
|
1225
|
+
.join('')
|
|
1226
|
+
|
|
1227
|
+
return `
|
|
1228
|
+
<div class="hooks-section">
|
|
1229
|
+
<h4>Hooks:</h4>
|
|
1230
|
+
<div class="hooks-list">${hooksHtml}</div>
|
|
1231
|
+
</div>
|
|
1232
|
+
`
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function generateMetadataHtml(meta, opts) {
|
|
1236
|
+
const allMeta = { ...(opts || {}), ...(meta || {}) }
|
|
1237
|
+
if (!allMeta || Object.keys(allMeta).length === 0) return ''
|
|
1238
|
+
|
|
1239
|
+
const metaHtml = Object.entries(allMeta)
|
|
1240
|
+
.filter(([key, value]) => value !== undefined && value !== null)
|
|
1241
|
+
.map(([key, value]) => {
|
|
1242
|
+
const displayValue = typeof value === 'object' ? JSON.stringify(value) : value.toString()
|
|
1243
|
+
return `<div class="meta-item"><span class="meta-key">${escapeHtml(key)}:</span> <span class="meta-value">${escapeHtml(displayValue)}</span></div>`
|
|
1244
|
+
})
|
|
1245
|
+
.join('')
|
|
1246
|
+
|
|
1247
|
+
return `
|
|
1248
|
+
<div class="metadata-section">
|
|
1249
|
+
<h4>Metadata:</h4>
|
|
1250
|
+
<div class="metadata-list">${metaHtml}</div>
|
|
1251
|
+
</div>
|
|
1252
|
+
`
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function generateTagsHtml(tags) {
|
|
1256
|
+
if (!tags || tags.length === 0) return ''
|
|
1257
|
+
|
|
1258
|
+
const tagsHtml = tags.map(tag => `<span class="test-tag">${escapeHtml(tag)}</span>`).join('')
|
|
1259
|
+
|
|
1260
|
+
return `
|
|
1261
|
+
<div class="tags-section">
|
|
1262
|
+
<h4>Tags:</h4>
|
|
1263
|
+
<div class="tags-list">${tagsHtml}</div>
|
|
1264
|
+
</div>
|
|
1265
|
+
`
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function generateNotesHtml(notes) {
|
|
1269
|
+
if (!notes || notes.length === 0) return ''
|
|
1270
|
+
|
|
1271
|
+
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('')
|
|
1272
|
+
|
|
1273
|
+
return `
|
|
1274
|
+
<div class="notes-section">
|
|
1275
|
+
<h4>Notes:</h4>
|
|
1276
|
+
<div class="notes-list">${notesHtml}</div>
|
|
1277
|
+
</div>
|
|
1278
|
+
`
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function generateTestRetryHtml(retryAttempts, testState) {
|
|
1282
|
+
// Enhanced retry history display showing whether test eventually passed or failed
|
|
1283
|
+
const statusBadge = testState === 'passed' ? '<span class="retry-status-badge passed">✓ Eventually Passed</span>' : '<span class="retry-status-badge failed">✗ Eventually Failed</span>'
|
|
1284
|
+
|
|
1285
|
+
return `
|
|
1286
|
+
<div class="retry-section">
|
|
1287
|
+
<h4>Retry History:</h4>
|
|
1288
|
+
<div class="retry-info">
|
|
1289
|
+
<div class="retry-summary">
|
|
1290
|
+
<span class="retry-count">Total retry attempts: <strong>${retryAttempts}</strong></span>
|
|
1291
|
+
${statusBadge}
|
|
1292
|
+
</div>
|
|
1293
|
+
<div class="retry-description">
|
|
1294
|
+
This test was retried <strong>${retryAttempts}</strong> time${retryAttempts > 1 ? 's' : ''} before ${testState === 'passed' ? 'passing' : 'failing'}.
|
|
1295
|
+
</div>
|
|
1296
|
+
</div>
|
|
1297
|
+
</div>
|
|
1298
|
+
`
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function generateArtifactsHtml(artifacts, isFailedTest = false) {
|
|
1302
|
+
if (!artifacts || artifacts.length === 0) {
|
|
1303
|
+
output.debug(`HTML Reporter: No artifacts found for test`)
|
|
1304
|
+
return ''
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
output.debug(`HTML Reporter: Processing ${artifacts.length} artifacts, isFailedTest: ${isFailedTest}`)
|
|
1308
|
+
output.debug(`HTML Reporter: Artifacts: ${JSON.stringify(artifacts)}`)
|
|
1309
|
+
|
|
1310
|
+
// Separate screenshots from other artifacts
|
|
1311
|
+
const screenshots = []
|
|
1312
|
+
const otherArtifacts = []
|
|
1313
|
+
|
|
1314
|
+
artifacts.forEach(artifact => {
|
|
1315
|
+
output.debug(`HTML Reporter: Processing artifact: ${artifact} (type: ${typeof artifact})`)
|
|
1316
|
+
|
|
1317
|
+
// Handle different artifact formats
|
|
1318
|
+
let artifactPath = artifact
|
|
1319
|
+
if (typeof artifact === 'object' && artifact.path) {
|
|
1320
|
+
artifactPath = artifact.path
|
|
1321
|
+
} else if (typeof artifact === 'object' && artifact.file) {
|
|
1322
|
+
artifactPath = artifact.file
|
|
1323
|
+
} else if (typeof artifact === 'object' && artifact.src) {
|
|
1324
|
+
artifactPath = artifact.src
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Check if it's a screenshot file
|
|
1328
|
+
if (typeof artifactPath === 'string' && artifactPath.match(/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i)) {
|
|
1329
|
+
screenshots.push(artifactPath)
|
|
1330
|
+
output.debug(`HTML Reporter: Found screenshot: ${artifactPath}`)
|
|
1331
|
+
} else {
|
|
1332
|
+
otherArtifacts.push(artifact)
|
|
1333
|
+
output.debug(`HTML Reporter: Found other artifact: ${artifact}`)
|
|
1334
|
+
}
|
|
1335
|
+
})
|
|
1336
|
+
|
|
1337
|
+
output.debug(`HTML Reporter: Found ${screenshots.length} screenshots and ${otherArtifacts.length} other artifacts`)
|
|
1338
|
+
|
|
1339
|
+
let artifactsHtml = ''
|
|
1340
|
+
|
|
1341
|
+
// For failed tests, prominently display screenshots
|
|
1342
|
+
if (isFailedTest && screenshots.length > 0) {
|
|
1343
|
+
const screenshotsHtml = screenshots
|
|
1344
|
+
.map(screenshot => {
|
|
1345
|
+
let relativePath = path.relative(reportDir, screenshot)
|
|
1346
|
+
const filename = path.basename(screenshot)
|
|
1347
|
+
|
|
1348
|
+
// If relative path goes up directories, try to find the file in common locations
|
|
1349
|
+
if (relativePath.startsWith('..')) {
|
|
1350
|
+
// Try to find screenshot relative to output directory
|
|
1351
|
+
const outputRelativePath = path.relative(reportDir, path.resolve(screenshot))
|
|
1352
|
+
if (!outputRelativePath.startsWith('..')) {
|
|
1353
|
+
relativePath = outputRelativePath
|
|
1354
|
+
} else {
|
|
1355
|
+
// Use just the filename if file is in same directory as report
|
|
1356
|
+
const sameDir = path.join(reportDir, filename)
|
|
1357
|
+
if (fs.existsSync(sameDir)) {
|
|
1358
|
+
relativePath = filename
|
|
1359
|
+
} else {
|
|
1360
|
+
// Keep original path as fallback
|
|
1361
|
+
relativePath = screenshot
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
output.debug(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`)
|
|
1367
|
+
|
|
1368
|
+
return `
|
|
1369
|
+
<div class="screenshot-container">
|
|
1370
|
+
<div class="screenshot-header">
|
|
1371
|
+
<span class="screenshot-label">📸 ${escapeHtml(filename)}</span>
|
|
1372
|
+
</div>
|
|
1373
|
+
<img src="${relativePath}" alt="Test failure screenshot" class="failure-screenshot" onclick="openImageModal(this.src)"/>
|
|
1374
|
+
</div>
|
|
1375
|
+
`
|
|
1376
|
+
})
|
|
1377
|
+
.join('')
|
|
1378
|
+
|
|
1379
|
+
artifactsHtml += `
|
|
1380
|
+
<div class="screenshots-section">
|
|
1381
|
+
<h4>Screenshots:</h4>
|
|
1382
|
+
<div class="screenshots-list">${screenshotsHtml}</div>
|
|
1383
|
+
</div>
|
|
1384
|
+
`
|
|
1385
|
+
} else if (screenshots.length > 0) {
|
|
1386
|
+
// For non-failed tests, display screenshots normally
|
|
1387
|
+
const screenshotsHtml = screenshots
|
|
1388
|
+
.map(screenshot => {
|
|
1389
|
+
let relativePath = path.relative(reportDir, screenshot)
|
|
1390
|
+
const filename = path.basename(screenshot)
|
|
1391
|
+
|
|
1392
|
+
// If relative path goes up directories, try to find the file in common locations
|
|
1393
|
+
if (relativePath.startsWith('..')) {
|
|
1394
|
+
// Try to find screenshot relative to output directory
|
|
1395
|
+
const outputRelativePath = path.relative(reportDir, path.resolve(screenshot))
|
|
1396
|
+
if (!outputRelativePath.startsWith('..')) {
|
|
1397
|
+
relativePath = outputRelativePath
|
|
1398
|
+
} else {
|
|
1399
|
+
// Use just the filename if file is in same directory as report
|
|
1400
|
+
const sameDir = path.join(reportDir, filename)
|
|
1401
|
+
if (fs.existsSync(sameDir)) {
|
|
1402
|
+
relativePath = filename
|
|
1403
|
+
} else {
|
|
1404
|
+
// Keep original path as fallback
|
|
1405
|
+
relativePath = screenshot
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
output.debug(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`)
|
|
1411
|
+
return `<img src="${relativePath}" alt="Screenshot" class="artifact-image" onclick="openImageModal(this.src)"/>`
|
|
1412
|
+
})
|
|
1413
|
+
.join('')
|
|
1414
|
+
|
|
1415
|
+
artifactsHtml += `
|
|
1416
|
+
<div class="screenshots-section">
|
|
1417
|
+
<h4>Screenshots:</h4>
|
|
1418
|
+
<div class="screenshots-list">${screenshotsHtml}</div>
|
|
1419
|
+
</div>
|
|
1420
|
+
`
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Display other artifacts if any
|
|
1424
|
+
if (otherArtifacts.length > 0) {
|
|
1425
|
+
const otherArtifactsHtml = otherArtifacts.map(artifact => `<div class="artifact-item">${escapeHtml(artifact.toString())}</div>`).join('')
|
|
1426
|
+
|
|
1427
|
+
artifactsHtml += `
|
|
1428
|
+
<div class="other-artifacts-section">
|
|
1429
|
+
<h4>Other Artifacts:</h4>
|
|
1430
|
+
<div class="artifacts-list">${otherArtifactsHtml}</div>
|
|
1431
|
+
</div>
|
|
1432
|
+
`
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
return artifactsHtml
|
|
1436
|
+
? `
|
|
1437
|
+
<div class="artifacts-section">
|
|
1438
|
+
${artifactsHtml}
|
|
1439
|
+
</div>
|
|
1440
|
+
`
|
|
1441
|
+
: ''
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function generateFailuresHtml(failures) {
|
|
1445
|
+
if (!failures || failures.length === 0) {
|
|
1446
|
+
return '<p>No failures.</p>'
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
return failures
|
|
1450
|
+
.map((failure, index) => {
|
|
1451
|
+
// Helper function to safely extract string values
|
|
1452
|
+
const safeString = value => {
|
|
1453
|
+
if (!value) return ''
|
|
1454
|
+
if (typeof value === 'string') return value
|
|
1455
|
+
if (typeof value === 'object' && value.toString) {
|
|
1456
|
+
const str = value.toString()
|
|
1457
|
+
return str === '[object Object]' ? '' : str
|
|
1458
|
+
}
|
|
1459
|
+
return String(value)
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (typeof failure === 'object' && failure !== null) {
|
|
1463
|
+
// Enhanced failure object with test details
|
|
1464
|
+
console.log('this is failure', failure)
|
|
1465
|
+
const testName = safeString(failure.testName) || 'Unknown Test'
|
|
1466
|
+
const featureName = safeString(failure.featureName) || 'Unknown Feature'
|
|
1467
|
+
let message = safeString(failure.message) || 'Test failed'
|
|
1468
|
+
const stack = safeString(failure.stack) || ''
|
|
1469
|
+
const filePath = safeString(failure.filePath) || ''
|
|
1470
|
+
|
|
1471
|
+
// If message is still "[object Object]", try to extract from the failure object itself
|
|
1472
|
+
if (message === '[object Object]' || message === '') {
|
|
1473
|
+
if (failure.err && failure.err.message) {
|
|
1474
|
+
message = safeString(failure.err.message)
|
|
1475
|
+
} else if (failure.error && failure.error.message) {
|
|
1476
|
+
message = safeString(failure.error.message)
|
|
1477
|
+
} else if (failure.toString && typeof failure.toString === 'function') {
|
|
1478
|
+
const str = failure.toString()
|
|
1479
|
+
message = str === '[object Object]' ? 'Test failed' : str
|
|
1480
|
+
} else {
|
|
1481
|
+
message = 'Test failed'
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
return `
|
|
1486
|
+
<div class="failure-item">
|
|
1487
|
+
<h4>Failure ${index + 1}: ${escapeHtml(testName)}</h4>
|
|
1488
|
+
<div class="failure-meta">
|
|
1489
|
+
<span class="failure-feature">Feature: ${escapeHtml(featureName)}</span>
|
|
1490
|
+
${filePath ? `<span class="failure-file">File: <a href="file://${filePath}" target="_blank">${escapeHtml(filePath)}</a></span>` : ''}
|
|
1491
|
+
</div>
|
|
1492
|
+
<div class="failure-message">
|
|
1493
|
+
<strong>Error:</strong> ${escapeHtml(message)}
|
|
1494
|
+
</div>
|
|
1495
|
+
${stack ? `<pre class="failure-stack">${escapeHtml(stack.replace(/\x1b\[[0-9;]*m/g, ''))}</pre>` : ''}
|
|
1496
|
+
</div>
|
|
1497
|
+
`
|
|
1498
|
+
} else {
|
|
1499
|
+
// Fallback for simple string failures
|
|
1500
|
+
const failureText = safeString(failure).replace(/\x1b\[[0-9;]*m/g, '') || 'Test failed'
|
|
1501
|
+
return `
|
|
1502
|
+
<div class="failure-item">
|
|
1503
|
+
<h4>Failure ${index + 1}</h4>
|
|
1504
|
+
<pre class="failure-details">${escapeHtml(failureText)}</pre>
|
|
1505
|
+
</div>
|
|
1506
|
+
`
|
|
1507
|
+
}
|
|
1508
|
+
})
|
|
1509
|
+
.join('')
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function generateRetriesHtml(retries) {
|
|
1513
|
+
if (!retries || retries.length === 0) {
|
|
1514
|
+
return '<p>No retried tests.</p>'
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
return retries
|
|
1518
|
+
.map(
|
|
1519
|
+
retry => `
|
|
1520
|
+
<div class="retry-item">
|
|
1521
|
+
<h4>${retry.testTitle}</h4>
|
|
1522
|
+
<div class="retry-details">
|
|
1523
|
+
<span>Attempts: <strong>${retry.attempts}</strong></span>
|
|
1524
|
+
<span>Final State: <span class="status-badge ${retry.finalState}">${retry.finalState}</span></span>
|
|
1525
|
+
<span>Duration: ${formatDuration(retry.duration)}</span>
|
|
1526
|
+
</div>
|
|
1527
|
+
</div>
|
|
1528
|
+
`,
|
|
1529
|
+
)
|
|
1530
|
+
.join('')
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function formatDuration(duration) {
|
|
1534
|
+
if (!duration) return '0ms'
|
|
1535
|
+
if (duration < 1000) return `${duration}ms`
|
|
1536
|
+
return `${(duration / 1000).toFixed(2)}s`
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function escapeHtml(text) {
|
|
1540
|
+
if (!text) return ''
|
|
1541
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function getErrorMessage(test) {
|
|
1545
|
+
if (!test) return 'Test failed'
|
|
1546
|
+
|
|
1547
|
+
// Helper function to safely extract string from potentially circular objects
|
|
1548
|
+
const safeExtract = (obj, prop) => {
|
|
1549
|
+
try {
|
|
1550
|
+
if (!obj || typeof obj !== 'object') return ''
|
|
1551
|
+
const value = obj[prop]
|
|
1552
|
+
if (typeof value === 'string') return value
|
|
1553
|
+
if (value && typeof value.toString === 'function') {
|
|
1554
|
+
const str = value.toString()
|
|
1555
|
+
return str === '[object Object]' ? '' : str
|
|
1556
|
+
}
|
|
1557
|
+
return ''
|
|
1558
|
+
} catch (e) {
|
|
1559
|
+
return ''
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Helper function to safely stringify objects avoiding circular references
|
|
1564
|
+
const safeStringify = obj => {
|
|
1565
|
+
try {
|
|
1566
|
+
if (!obj) return ''
|
|
1567
|
+
if (typeof obj === 'string') return obj
|
|
1568
|
+
|
|
1569
|
+
// Try to get message property first
|
|
1570
|
+
if (obj.message && typeof obj.message === 'string') {
|
|
1571
|
+
return obj.message
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// For error objects, extract key properties manually
|
|
1575
|
+
if (obj instanceof Error || (obj.name && obj.message)) {
|
|
1576
|
+
return obj.message || obj.toString() || 'Error occurred'
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// For other objects, try toString first
|
|
1580
|
+
if (obj.toString && typeof obj.toString === 'function') {
|
|
1581
|
+
const str = obj.toString()
|
|
1582
|
+
if (str !== '[object Object]' && !str.includes('[Circular Reference]')) {
|
|
1583
|
+
return str
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Last resort: extract message-like properties
|
|
1588
|
+
if (obj.message) return obj.message
|
|
1589
|
+
if (obj.description) return obj.description
|
|
1590
|
+
if (obj.text) return obj.text
|
|
1591
|
+
|
|
1592
|
+
return 'Error occurred'
|
|
1593
|
+
} catch (e) {
|
|
1594
|
+
return 'Error occurred'
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
let errorMessage = ''
|
|
1599
|
+
let errorStack = ''
|
|
1600
|
+
|
|
1601
|
+
// Primary error source
|
|
1602
|
+
if (test.err) {
|
|
1603
|
+
errorMessage = safeExtract(test.err, 'message') || safeStringify(test.err)
|
|
1604
|
+
errorStack = safeExtract(test.err, 'stack')
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Alternative error sources for different test frameworks
|
|
1608
|
+
if (!errorMessage && test.error) {
|
|
1609
|
+
errorMessage = safeExtract(test.error, 'message') || safeStringify(test.error)
|
|
1610
|
+
errorStack = safeExtract(test.error, 'stack')
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Check for nested error in parent
|
|
1614
|
+
if (!errorMessage && test.parent && test.parent.err) {
|
|
1615
|
+
errorMessage = safeExtract(test.parent.err, 'message') || safeStringify(test.parent.err)
|
|
1616
|
+
errorStack = safeExtract(test.parent.err, 'stack')
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// Check for error details array (some frameworks use this)
|
|
1620
|
+
if (!errorMessage && test.err && test.err.details && Array.isArray(test.err.details)) {
|
|
1621
|
+
errorMessage = test.err.details
|
|
1622
|
+
.map(item => safeExtract(item, 'message') || safeStringify(item))
|
|
1623
|
+
.filter(msg => msg && msg !== '[Circular]')
|
|
1624
|
+
.join(' ')
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// Fallback to test title if no error message found
|
|
1628
|
+
if (!errorMessage || errorMessage === '[Circular]') {
|
|
1629
|
+
errorMessage = `Test failed: ${test.title || 'Unknown test'}`
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// Clean ANSI escape codes and remove circular reference markers
|
|
1633
|
+
const cleanMessage = (errorMessage || '')
|
|
1634
|
+
.replace(/\x1b\[[0-9;]*m/g, '')
|
|
1635
|
+
.replace(/\[Circular\]/g, '')
|
|
1636
|
+
.replace(/\s+/g, ' ')
|
|
1637
|
+
.trim()
|
|
1638
|
+
|
|
1639
|
+
const cleanStack = (errorStack || '')
|
|
1640
|
+
.replace(/\x1b\[[0-9;]*m/g, '')
|
|
1641
|
+
.replace(/\[Circular\]/g, '')
|
|
1642
|
+
.trim()
|
|
1643
|
+
|
|
1644
|
+
// Return combined error information
|
|
1645
|
+
if (cleanStack && cleanStack !== cleanMessage && !cleanMessage.includes(cleanStack)) {
|
|
1646
|
+
return `${cleanMessage}\n\nStack trace:\n${cleanStack}`
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
return cleanMessage
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function generateSystemInfoHtml(systemInfo) {
|
|
1653
|
+
if (!systemInfo) return ''
|
|
1654
|
+
|
|
1655
|
+
const formatInfo = (key, value) => {
|
|
1656
|
+
if (Array.isArray(value) && value.length > 1) {
|
|
1657
|
+
return `<div class="info-item"><span class="info-key">${key}:</span> <span class="info-value">${escapeHtml(value[1])}</span></div>`
|
|
1658
|
+
} else if (typeof value === 'string' && value !== 'N/A' && value !== 'undefined') {
|
|
1659
|
+
return `<div class="info-item"><span class="info-key">${key}:</span> <span class="info-value">${escapeHtml(value)}</span></div>`
|
|
1660
|
+
}
|
|
1661
|
+
return ''
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const infoItems = [
|
|
1665
|
+
formatInfo('Node.js', systemInfo.nodeInfo),
|
|
1666
|
+
formatInfo('OS', systemInfo.osInfo),
|
|
1667
|
+
formatInfo('CPU', systemInfo.cpuInfo),
|
|
1668
|
+
formatInfo('Chrome', systemInfo.chromeInfo),
|
|
1669
|
+
formatInfo('Edge', systemInfo.edgeInfo),
|
|
1670
|
+
formatInfo('Firefox', systemInfo.firefoxInfo),
|
|
1671
|
+
formatInfo('Safari', systemInfo.safariInfo),
|
|
1672
|
+
formatInfo('Playwright Browsers', systemInfo.playwrightBrowsers),
|
|
1673
|
+
]
|
|
1674
|
+
.filter(item => item)
|
|
1675
|
+
.join('')
|
|
1676
|
+
|
|
1677
|
+
if (!infoItems) return ''
|
|
1678
|
+
|
|
1679
|
+
return `
|
|
1680
|
+
<section class="system-info-section">
|
|
1681
|
+
<div class="system-info-header" onclick="toggleSystemInfo()">
|
|
1682
|
+
<h3>Environment Information</h3>
|
|
1683
|
+
<span class="toggle-icon">▼</span>
|
|
1684
|
+
</div>
|
|
1685
|
+
<div class="system-info-content" id="systemInfoContent">
|
|
1686
|
+
<div class="system-info-grid">
|
|
1687
|
+
${infoItems}
|
|
1688
|
+
</div>
|
|
1689
|
+
</div>
|
|
1690
|
+
</section>
|
|
1691
|
+
`
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function getHtmlTemplate() {
|
|
1695
|
+
return `
|
|
1696
|
+
<!DOCTYPE html>
|
|
1697
|
+
<html lang="en">
|
|
1698
|
+
<head>
|
|
1699
|
+
<meta charset="UTF-8">
|
|
1700
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1701
|
+
<title>{{title}}</title>
|
|
1702
|
+
<style>{{cssStyles}}</style>
|
|
1703
|
+
</head>
|
|
1704
|
+
<body>
|
|
1705
|
+
<header class="report-header">
|
|
1706
|
+
<h1>{{title}}</h1>
|
|
1707
|
+
<div class="report-meta">
|
|
1708
|
+
<span>Generated: {{timestamp}}</span>
|
|
1709
|
+
<span>Duration: {{duration}}</span>
|
|
1710
|
+
</div>
|
|
1711
|
+
</header>
|
|
1712
|
+
|
|
1713
|
+
<main class="report-content">
|
|
1714
|
+
{{systemInfoHtml}}
|
|
1715
|
+
|
|
1716
|
+
<section class="stats-section">
|
|
1717
|
+
<h2>Test Statistics</h2>
|
|
1718
|
+
{{statsHtml}}
|
|
1719
|
+
</section>
|
|
1720
|
+
|
|
1721
|
+
<section class="test-performance-section">
|
|
1722
|
+
<h2>Test Performance Analysis</h2>
|
|
1723
|
+
<div class="performance-container">
|
|
1724
|
+
<div class="performance-group">
|
|
1725
|
+
<h3>⏱️ Longest Running Tests</h3>
|
|
1726
|
+
<div id="longestTests" class="performance-list"></div>
|
|
1727
|
+
</div>
|
|
1728
|
+
<div class="performance-group">
|
|
1729
|
+
<h3>⚡ Fastest Tests</h3>
|
|
1730
|
+
<div id="fastestTests" class="performance-list"></div>
|
|
1731
|
+
</div>
|
|
1732
|
+
</div>
|
|
1733
|
+
</section>
|
|
1734
|
+
|
|
1735
|
+
<section class="history-section" style="display: {{showHistory}};">
|
|
1736
|
+
<h2>Test Execution History</h2>
|
|
1737
|
+
<div class="history-stats" id="historyStats"></div>
|
|
1738
|
+
<div class="history-timeline" id="historyTimeline"></div>
|
|
1739
|
+
<div class="history-chart-container">
|
|
1740
|
+
<canvas id="historyChart" width="1600" height="600"></canvas>
|
|
1741
|
+
</div>
|
|
1742
|
+
</section>
|
|
1743
|
+
|
|
1744
|
+
<section class="filters-section">
|
|
1745
|
+
<h2>Filters</h2>
|
|
1746
|
+
<div class="filter-controls">
|
|
1747
|
+
<div class="filter-group">
|
|
1748
|
+
<label>Status:</label>
|
|
1749
|
+
<select id="statusFilter" multiple>
|
|
1750
|
+
<option value="passed">Passed</option>
|
|
1751
|
+
<option value="failed">Failed</option>
|
|
1752
|
+
<option value="pending">Pending</option>
|
|
1753
|
+
<option value="skipped">Skipped</option>
|
|
1754
|
+
</select>
|
|
1755
|
+
</div>
|
|
1756
|
+
<div class="filter-group">
|
|
1757
|
+
<label>Feature:</label>
|
|
1758
|
+
<input type="text" id="featureFilter" placeholder="Filter by feature...">
|
|
1759
|
+
</div>
|
|
1760
|
+
<div class="filter-group">
|
|
1761
|
+
<label>Tags:</label>
|
|
1762
|
+
<input type="text" id="tagFilter" placeholder="Filter by tags...">
|
|
1763
|
+
</div>
|
|
1764
|
+
<div class="filter-group">
|
|
1765
|
+
<label>Retries:</label>
|
|
1766
|
+
<select id="retryFilter">
|
|
1767
|
+
<option value="all">All</option>
|
|
1768
|
+
<option value="retried">With Retries</option>
|
|
1769
|
+
<option value="no-retries">No Retries</option>
|
|
1770
|
+
</select>
|
|
1771
|
+
</div>
|
|
1772
|
+
<div class="filter-group">
|
|
1773
|
+
<label>Test Type:</label>
|
|
1774
|
+
<select id="typeFilter">
|
|
1775
|
+
<option value="all">All</option>
|
|
1776
|
+
<option value="bdd">BDD/Gherkin</option>
|
|
1777
|
+
<option value="regular">Regular</option>
|
|
1778
|
+
</select>
|
|
1779
|
+
</div>
|
|
1780
|
+
<button onclick="resetFilters()">Reset Filters</button>
|
|
1781
|
+
</div>
|
|
1782
|
+
</section>
|
|
1783
|
+
|
|
1784
|
+
<section class="tests-section">
|
|
1785
|
+
<h2>Test Results</h2>
|
|
1786
|
+
<div class="tests-container">
|
|
1787
|
+
{{testsHtml}}
|
|
1788
|
+
</div>
|
|
1789
|
+
</section>
|
|
1790
|
+
|
|
1791
|
+
<section class="retries-section" style="display: none;">
|
|
1792
|
+
<h2>Test Retries (Moved to Test Details)</h2>
|
|
1793
|
+
<div class="retries-container">
|
|
1794
|
+
<p>Retry information is now shown in each test's details section.</p>
|
|
1795
|
+
</div>
|
|
1796
|
+
</section>
|
|
1797
|
+
|
|
1798
|
+
</main>
|
|
1799
|
+
|
|
1800
|
+
<!-- Modal for images -->
|
|
1801
|
+
<div id="imageModal" class="modal" onclick="closeImageModal()">
|
|
1802
|
+
<img id="modalImage" src="" alt="Enlarged screenshot"/>
|
|
1803
|
+
</div>
|
|
1804
|
+
|
|
1805
|
+
<script>
|
|
1806
|
+
window.testData = {
|
|
1807
|
+
stats: {{stats}},
|
|
1808
|
+
history: {{history}}
|
|
1809
|
+
};
|
|
1810
|
+
</script>
|
|
1811
|
+
<script>{{jsScripts}}</script>
|
|
1812
|
+
</body>
|
|
1813
|
+
</html>
|
|
1814
|
+
`
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
function getCssStyles() {
|
|
1818
|
+
return `
|
|
1819
|
+
* {
|
|
1820
|
+
margin: 0;
|
|
1821
|
+
padding: 0;
|
|
1822
|
+
box-sizing: border-box;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
body {
|
|
1826
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
1827
|
+
line-height: 1.6;
|
|
1828
|
+
color: #333;
|
|
1829
|
+
background-color: #f5f5f5;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
.report-header {
|
|
1833
|
+
background: #2c3e50;
|
|
1834
|
+
color: white;
|
|
1835
|
+
padding: 2rem 1rem;
|
|
1836
|
+
text-align: center;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
.report-header h1 {
|
|
1840
|
+
margin-bottom: 0.5rem;
|
|
1841
|
+
font-size: 2.5rem;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
.report-meta {
|
|
1845
|
+
font-size: 0.9rem;
|
|
1846
|
+
opacity: 0.8;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
.report-meta span {
|
|
1850
|
+
margin: 0 1rem;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
.report-content {
|
|
1854
|
+
max-width: 1200px;
|
|
1855
|
+
margin: 2rem auto;
|
|
1856
|
+
padding: 0 1rem;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
.stats-section, .tests-section, .retries-section, .filters-section, .history-section, .system-info-section, .test-performance-section {
|
|
1860
|
+
background: white;
|
|
1861
|
+
margin-bottom: 2rem;
|
|
1862
|
+
border-radius: 8px;
|
|
1863
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
1864
|
+
overflow: hidden;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
.stats-section h2, .tests-section h2, .retries-section h2, .filters-section h2, .history-section h2, .test-performance-section h2 {
|
|
1868
|
+
background: #34495e;
|
|
1869
|
+
color: white;
|
|
1870
|
+
padding: 1rem;
|
|
1871
|
+
margin: 0;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
.stats-cards {
|
|
1875
|
+
display: flex;
|
|
1876
|
+
flex-wrap: wrap;
|
|
1877
|
+
gap: 1rem;
|
|
1878
|
+
padding: 1rem;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
.stat-card {
|
|
1882
|
+
flex: 1;
|
|
1883
|
+
min-width: 150px;
|
|
1884
|
+
padding: 1rem;
|
|
1885
|
+
text-align: center;
|
|
1886
|
+
border-radius: 4px;
|
|
1887
|
+
color: white;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
.stat-card.total { background: #3498db; }
|
|
1891
|
+
.stat-card.passed { background: #27ae60; }
|
|
1892
|
+
.stat-card.failed { background: #e74c3c; }
|
|
1893
|
+
.stat-card.pending { background: #f39c12; }
|
|
1894
|
+
.stat-card.flaky { background: #e67e22; }
|
|
1895
|
+
.stat-card.artifacts { background: #9b59b6; }
|
|
1896
|
+
|
|
1897
|
+
.metrics-summary {
|
|
1898
|
+
display: flex;
|
|
1899
|
+
justify-content: center;
|
|
1900
|
+
gap: 2rem;
|
|
1901
|
+
padding: 1rem;
|
|
1902
|
+
background: #f8f9fa;
|
|
1903
|
+
border-radius: 6px;
|
|
1904
|
+
margin: 1rem 0;
|
|
1905
|
+
font-size: 1rem;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
.metrics-summary span {
|
|
1909
|
+
color: #34495e;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
.stat-card h3 {
|
|
1913
|
+
font-size: 0.9rem;
|
|
1914
|
+
margin-bottom: 0.5rem;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
.stat-number {
|
|
1918
|
+
font-size: 2rem;
|
|
1919
|
+
font-weight: bold;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
.pie-chart-container {
|
|
1923
|
+
display: flex;
|
|
1924
|
+
justify-content: center;
|
|
1925
|
+
align-items: center;
|
|
1926
|
+
padding: 2rem 1rem;
|
|
1927
|
+
background: white;
|
|
1928
|
+
margin: 1rem 0;
|
|
1929
|
+
border-radius: 8px;
|
|
1930
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
#statsChart {
|
|
1934
|
+
max-width: 100%;
|
|
1935
|
+
height: auto;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
.feature-group {
|
|
1939
|
+
margin-bottom: 2.5rem;
|
|
1940
|
+
border: 2px solid #3498db;
|
|
1941
|
+
border-radius: 12px;
|
|
1942
|
+
overflow: hidden;
|
|
1943
|
+
background: white;
|
|
1944
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
.feature-group-title {
|
|
1948
|
+
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
|
1949
|
+
color: white;
|
|
1950
|
+
padding: 1.2rem 1.5rem;
|
|
1951
|
+
margin: 0;
|
|
1952
|
+
font-size: 1.4rem;
|
|
1953
|
+
font-weight: 600;
|
|
1954
|
+
display: flex;
|
|
1955
|
+
align-items: center;
|
|
1956
|
+
justify-content: space-between;
|
|
1957
|
+
cursor: pointer;
|
|
1958
|
+
transition: all 0.3s ease;
|
|
1959
|
+
user-select: none;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
.feature-group-title:hover {
|
|
1963
|
+
background: linear-gradient(135deg, #2980b9 0%, #21618c 100%);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
.feature-group-title .toggle-icon {
|
|
1967
|
+
font-size: 1.2rem;
|
|
1968
|
+
transition: transform 0.3s ease;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
.feature-group-title .toggle-icon.rotated {
|
|
1972
|
+
transform: rotate(180deg);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
.feature-tests {
|
|
1976
|
+
padding: 0;
|
|
1977
|
+
transition: max-height 0.3s ease, opacity 0.3s ease;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
.feature-tests.collapsed {
|
|
1981
|
+
max-height: 0;
|
|
1982
|
+
opacity: 0;
|
|
1983
|
+
overflow: hidden;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
.test-item {
|
|
1987
|
+
border-bottom: 1px solid #eee;
|
|
1988
|
+
margin: 0;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
.test-item:last-child {
|
|
1992
|
+
border-bottom: none;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
.test-header {
|
|
1996
|
+
display: flex;
|
|
1997
|
+
align-items: center;
|
|
1998
|
+
padding: 1rem;
|
|
1999
|
+
cursor: pointer;
|
|
2000
|
+
transition: background-color 0.2s;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
.test-header:hover {
|
|
2004
|
+
background-color: #f8f9fa;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
.test-info {
|
|
2008
|
+
flex: 1;
|
|
2009
|
+
display: flex;
|
|
2010
|
+
flex-direction: column;
|
|
2011
|
+
gap: 0.25rem;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
.test-meta-line {
|
|
2015
|
+
display: flex;
|
|
2016
|
+
align-items: center;
|
|
2017
|
+
gap: 0.5rem;
|
|
2018
|
+
font-size: 0.9rem;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
.test-status {
|
|
2022
|
+
font-size: 1.2rem;
|
|
2023
|
+
margin-right: 0.5rem;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
.test-status.passed { color: #27ae60; }
|
|
2027
|
+
.test-status.failed { color: #e74c3c; }
|
|
2028
|
+
.test-status.pending { color: #f39c12; }
|
|
2029
|
+
.test-status.skipped { color: #95a5a6; }
|
|
2030
|
+
|
|
2031
|
+
.test-title {
|
|
2032
|
+
font-size: 1.1rem;
|
|
2033
|
+
font-weight: 500;
|
|
2034
|
+
margin: 0;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
.test-feature {
|
|
2038
|
+
background: #ecf0f1;
|
|
2039
|
+
padding: 0.25rem 0.5rem;
|
|
2040
|
+
border-radius: 4px;
|
|
2041
|
+
font-size: 0.8rem;
|
|
2042
|
+
color: #34495e;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
.test-uid {
|
|
2046
|
+
background: #e8f4fd;
|
|
2047
|
+
padding: 0.25rem 0.5rem;
|
|
2048
|
+
border-radius: 4px;
|
|
2049
|
+
font-size: 0.7rem;
|
|
2050
|
+
color: #2980b9;
|
|
2051
|
+
font-family: monospace;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
.retry-badge {
|
|
2055
|
+
background: #f39c12;
|
|
2056
|
+
color: white;
|
|
2057
|
+
padding: 0.25rem 0.5rem;
|
|
2058
|
+
border-radius: 4px;
|
|
2059
|
+
font-size: 0.7rem;
|
|
2060
|
+
font-weight: bold;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
.worker-badge {
|
|
2064
|
+
background: #16a085;
|
|
2065
|
+
color: white;
|
|
2066
|
+
padding: 0.25rem 0.5rem;
|
|
2067
|
+
border-radius: 4px;
|
|
2068
|
+
font-size: 0.7rem;
|
|
2069
|
+
font-weight: bold;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
/* Different colors for each worker index */
|
|
2073
|
+
.worker-badge.worker-0 {
|
|
2074
|
+
background: #3498db; /* Blue */
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
.worker-badge.worker-1 {
|
|
2078
|
+
background: #e74c3c; /* Red */
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
.worker-badge.worker-2 {
|
|
2082
|
+
background: #2ecc71; /* Green */
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
.worker-badge.worker-3 {
|
|
2086
|
+
background: #f39c12; /* Orange */
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
.worker-badge.worker-4 {
|
|
2090
|
+
background: #9b59b6; /* Purple */
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
.worker-badge.worker-5 {
|
|
2094
|
+
background: #1abc9c; /* Turquoise */
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
.worker-badge.worker-6 {
|
|
2098
|
+
background: #e67e22; /* Carrot */
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
.worker-badge.worker-7 {
|
|
2102
|
+
background: #34495e; /* Dark Blue-Gray */
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
.worker-badge.worker-8 {
|
|
2106
|
+
background: #16a085; /* Teal */
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
.worker-badge.worker-9 {
|
|
2110
|
+
background: #c0392b; /* Dark Red */
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
.test-duration {
|
|
2114
|
+
font-size: 0.85rem;
|
|
2115
|
+
font-weight: 600;
|
|
2116
|
+
color: #2c3e50;
|
|
2117
|
+
background: #ecf0f1;
|
|
2118
|
+
padding: 0.25rem 0.5rem;
|
|
2119
|
+
border-radius: 4px;
|
|
2120
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
.test-details {
|
|
2124
|
+
display: none;
|
|
2125
|
+
padding: 1rem;
|
|
2126
|
+
background: #f8f9fa;
|
|
2127
|
+
border-top: 1px solid #e9ecef;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
.error-message {
|
|
2131
|
+
background: #fee;
|
|
2132
|
+
border: 1px solid #fcc;
|
|
2133
|
+
border-radius: 4px;
|
|
2134
|
+
padding: 1rem;
|
|
2135
|
+
margin-bottom: 1rem;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
.error-message pre {
|
|
2139
|
+
color: #c0392b;
|
|
2140
|
+
font-family: 'Courier New', monospace;
|
|
2141
|
+
font-size: 0.9rem;
|
|
2142
|
+
white-space: pre-wrap;
|
|
2143
|
+
word-wrap: break-word;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
.steps-section, .artifacts-section, .hooks-section {
|
|
2147
|
+
margin-top: 1rem;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
.steps-section h4, .artifacts-section h4, .hooks-section h4 {
|
|
2151
|
+
color: #34495e;
|
|
2152
|
+
margin-bottom: 0.5rem;
|
|
2153
|
+
font-size: 1rem;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
.hook-item {
|
|
2157
|
+
display: flex;
|
|
2158
|
+
align-items: flex-start;
|
|
2159
|
+
padding: 0.75rem;
|
|
2160
|
+
border: 1px solid #ecf0f1;
|
|
2161
|
+
border-radius: 4px;
|
|
2162
|
+
margin-bottom: 0.5rem;
|
|
2163
|
+
background: #fafafa;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
.hook-item:last-child {
|
|
2167
|
+
margin-bottom: 0;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
.hook-status {
|
|
2171
|
+
margin-right: 0.75rem;
|
|
2172
|
+
flex-shrink: 0;
|
|
2173
|
+
margin-top: 0.2rem;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
.hook-status.passed { color: #27ae60; }
|
|
2177
|
+
.hook-status.failed { color: #e74c3c; }
|
|
2178
|
+
|
|
2179
|
+
.hook-content {
|
|
2180
|
+
flex: 1;
|
|
2181
|
+
display: flex;
|
|
2182
|
+
flex-direction: column;
|
|
2183
|
+
gap: 0.25rem;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
.hook-title {
|
|
2187
|
+
font-family: 'Courier New', monospace;
|
|
2188
|
+
font-size: 0.9rem;
|
|
2189
|
+
font-weight: bold;
|
|
2190
|
+
color: #2c3e50;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
.hook-duration {
|
|
2194
|
+
font-size: 0.8rem;
|
|
2195
|
+
color: #7f8c8d;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
.hook-location, .hook-context {
|
|
2199
|
+
font-size: 0.8rem;
|
|
2200
|
+
color: #6c757d;
|
|
2201
|
+
font-style: italic;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
.hook-error {
|
|
2205
|
+
margin-top: 0.5rem;
|
|
2206
|
+
padding: 0.5rem;
|
|
2207
|
+
background: #fee;
|
|
2208
|
+
border: 1px solid #fcc;
|
|
2209
|
+
border-radius: 4px;
|
|
2210
|
+
color: #c0392b;
|
|
2211
|
+
font-size: 0.8rem;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
.step-item {
|
|
2215
|
+
display: flex;
|
|
2216
|
+
align-items: flex-start;
|
|
2217
|
+
padding: 0.5rem 0;
|
|
2218
|
+
border-bottom: 1px solid #ecf0f1;
|
|
2219
|
+
word-wrap: break-word;
|
|
2220
|
+
overflow-wrap: break-word;
|
|
2221
|
+
min-height: 2rem;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
.step-item:last-child {
|
|
2225
|
+
border-bottom: none;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
.step-status {
|
|
2229
|
+
margin-right: 0.5rem;
|
|
2230
|
+
flex-shrink: 0;
|
|
2231
|
+
margin-top: 0.2rem;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
.step-status.success { color: #27ae60; }
|
|
2235
|
+
.step-status.failed { color: #e74c3c; }
|
|
2236
|
+
|
|
2237
|
+
.step-title {
|
|
2238
|
+
flex: 1;
|
|
2239
|
+
font-family: 'Courier New', monospace;
|
|
2240
|
+
font-size: 0.9rem;
|
|
2241
|
+
word-wrap: break-word;
|
|
2242
|
+
overflow-wrap: break-word;
|
|
2243
|
+
line-height: 1.4;
|
|
2244
|
+
margin-right: 0.5rem;
|
|
2245
|
+
min-width: 0;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
.step-duration {
|
|
2249
|
+
font-size: 0.8rem;
|
|
2250
|
+
color: #7f8c8d;
|
|
2251
|
+
flex-shrink: 0;
|
|
2252
|
+
margin-top: 0.2rem;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
.artifacts-list {
|
|
2256
|
+
display: flex;
|
|
2257
|
+
flex-wrap: wrap;
|
|
2258
|
+
gap: 0.5rem;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
.artifact-image {
|
|
2262
|
+
max-width: 200px;
|
|
2263
|
+
max-height: 150px;
|
|
2264
|
+
border: 1px solid #ddd;
|
|
2265
|
+
border-radius: 4px;
|
|
2266
|
+
cursor: pointer;
|
|
2267
|
+
transition: transform 0.2s;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
.artifact-image:hover {
|
|
2271
|
+
transform: scale(1.05);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
.artifact-item {
|
|
2275
|
+
background: #ecf0f1;
|
|
2276
|
+
padding: 0.5rem;
|
|
2277
|
+
border-radius: 4px;
|
|
2278
|
+
font-size: 0.9rem;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
.modal {
|
|
2282
|
+
display: none;
|
|
2283
|
+
position: fixed;
|
|
2284
|
+
z-index: 1000;
|
|
2285
|
+
left: 0;
|
|
2286
|
+
top: 0;
|
|
2287
|
+
width: 100%;
|
|
2288
|
+
height: 100%;
|
|
2289
|
+
background-color: rgba(0,0,0,0.8);
|
|
2290
|
+
cursor: pointer;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
.modal img {
|
|
2294
|
+
position: absolute;
|
|
2295
|
+
top: 50%;
|
|
2296
|
+
left: 50%;
|
|
2297
|
+
transform: translate(-50%, -50%);
|
|
2298
|
+
max-width: 90%;
|
|
2299
|
+
max-height: 90%;
|
|
2300
|
+
border-radius: 4px;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
/* Enhanced screenshot styles for failed tests */
|
|
2304
|
+
.screenshots-section {
|
|
2305
|
+
margin-top: 1rem;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
.screenshots-section h4 {
|
|
2309
|
+
color: #e74c3c;
|
|
2310
|
+
margin-bottom: 0.75rem;
|
|
2311
|
+
font-size: 1rem;
|
|
2312
|
+
font-weight: 600;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
.screenshots-list {
|
|
2316
|
+
display: flex;
|
|
2317
|
+
flex-direction: column;
|
|
2318
|
+
gap: 1rem;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
.screenshot-container {
|
|
2322
|
+
border: 2px solid #e74c3c;
|
|
2323
|
+
border-radius: 8px;
|
|
2324
|
+
overflow: hidden;
|
|
2325
|
+
background: white;
|
|
2326
|
+
box-shadow: 0 4px 8px rgba(231, 76, 60, 0.1);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
.screenshot-header {
|
|
2330
|
+
background: #e74c3c;
|
|
2331
|
+
color: white;
|
|
2332
|
+
padding: 0.5rem 1rem;
|
|
2333
|
+
font-size: 0.9rem;
|
|
2334
|
+
font-weight: 500;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
.screenshot-label {
|
|
2338
|
+
display: flex;
|
|
2339
|
+
align-items: center;
|
|
2340
|
+
gap: 0.5rem;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
.failure-screenshot {
|
|
2344
|
+
width: 100%;
|
|
2345
|
+
max-width: 100%;
|
|
2346
|
+
height: auto;
|
|
2347
|
+
display: block;
|
|
2348
|
+
cursor: pointer;
|
|
2349
|
+
transition: opacity 0.2s;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
.failure-screenshot:hover {
|
|
2353
|
+
opacity: 0.9;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
.other-artifacts-section {
|
|
2357
|
+
margin-top: 1rem;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
/* Filter Controls */
|
|
2361
|
+
.filter-controls {
|
|
2362
|
+
display: flex;
|
|
2363
|
+
flex-wrap: wrap;
|
|
2364
|
+
gap: 1rem;
|
|
2365
|
+
padding: 1rem;
|
|
2366
|
+
background: #f8f9fa;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
.filter-group {
|
|
2370
|
+
display: flex;
|
|
2371
|
+
flex-direction: column;
|
|
2372
|
+
gap: 0.25rem;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
.filter-group label {
|
|
2376
|
+
font-size: 0.9rem;
|
|
2377
|
+
font-weight: 500;
|
|
2378
|
+
color: #34495e;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
.filter-group input,
|
|
2382
|
+
.filter-group select {
|
|
2383
|
+
padding: 0.5rem;
|
|
2384
|
+
border: 1px solid #ddd;
|
|
2385
|
+
border-radius: 4px;
|
|
2386
|
+
font-size: 0.9rem;
|
|
2387
|
+
min-width: 150px;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
.filter-group select[multiple] {
|
|
2391
|
+
height: auto;
|
|
2392
|
+
min-height: 80px;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
.filter-controls button {
|
|
2396
|
+
padding: 0.5rem 1rem;
|
|
2397
|
+
background: #3498db;
|
|
2398
|
+
color: white;
|
|
2399
|
+
border: none;
|
|
2400
|
+
border-radius: 4px;
|
|
2401
|
+
cursor: pointer;
|
|
2402
|
+
font-size: 0.9rem;
|
|
2403
|
+
align-self: flex-end;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
.filter-controls button:hover {
|
|
2407
|
+
background: #2980b9;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
/* Test Tags */
|
|
2411
|
+
.tags-section, .metadata-section, .notes-section, .retry-section {
|
|
2412
|
+
margin-top: 1rem;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
.tags-list {
|
|
2416
|
+
display: flex;
|
|
2417
|
+
flex-wrap: wrap;
|
|
2418
|
+
gap: 0.5rem;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
.test-tag {
|
|
2422
|
+
background: #3498db;
|
|
2423
|
+
color: white;
|
|
2424
|
+
padding: 0.25rem 0.5rem;
|
|
2425
|
+
border-radius: 12px;
|
|
2426
|
+
font-size: 0.8rem;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
/* Metadata */
|
|
2430
|
+
.metadata-list {
|
|
2431
|
+
display: flex;
|
|
2432
|
+
flex-direction: column;
|
|
2433
|
+
gap: 0.5rem;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
.meta-item {
|
|
2437
|
+
padding: 0.5rem;
|
|
2438
|
+
background: #f8f9fa;
|
|
2439
|
+
border-radius: 4px;
|
|
2440
|
+
border-left: 3px solid #3498db;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
.meta-key {
|
|
2444
|
+
font-weight: bold;
|
|
2445
|
+
color: #2c3e50;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
.meta-value {
|
|
2449
|
+
color: #34495e;
|
|
2450
|
+
font-family: monospace;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
/* Notes */
|
|
2454
|
+
.notes-list {
|
|
2455
|
+
display: flex;
|
|
2456
|
+
flex-direction: column;
|
|
2457
|
+
gap: 0.5rem;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
.note-item {
|
|
2461
|
+
padding: 0.5rem;
|
|
2462
|
+
border-radius: 4px;
|
|
2463
|
+
border-left: 3px solid #95a5a6;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
.note-item.note-info {
|
|
2467
|
+
background: #e8f4fd;
|
|
2468
|
+
border-left-color: #3498db;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
.note-item.note-warning {
|
|
2472
|
+
background: #fef9e7;
|
|
2473
|
+
border-left-color: #f39c12;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
.note-item.note-error {
|
|
2477
|
+
background: #fee;
|
|
2478
|
+
border-left-color: #e74c3c;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
.note-item.note-retry {
|
|
2482
|
+
background: #f0f8e8;
|
|
2483
|
+
border-left-color: #27ae60;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
.note-type {
|
|
2487
|
+
font-weight: bold;
|
|
2488
|
+
text-transform: uppercase;
|
|
2489
|
+
font-size: 0.8rem;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
/* Retry Information */
|
|
2493
|
+
.retry-section {
|
|
2494
|
+
margin-top: 1rem;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
.retry-info {
|
|
2498
|
+
padding: 1rem;
|
|
2499
|
+
background: #fff9e6;
|
|
2500
|
+
border-radius: 4px;
|
|
2501
|
+
border-left: 4px solid #f39c12;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
.retry-summary {
|
|
2505
|
+
display: flex;
|
|
2506
|
+
align-items: center;
|
|
2507
|
+
gap: 1rem;
|
|
2508
|
+
margin-bottom: 0.5rem;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
.retry-count {
|
|
2512
|
+
color: #d68910;
|
|
2513
|
+
font-weight: 500;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
.retry-status-badge {
|
|
2517
|
+
padding: 0.25rem 0.75rem;
|
|
2518
|
+
border-radius: 4px;
|
|
2519
|
+
font-size: 0.85rem;
|
|
2520
|
+
font-weight: bold;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
.retry-status-badge.passed {
|
|
2524
|
+
background: #27ae60;
|
|
2525
|
+
color: white;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
.retry-status-badge.failed {
|
|
2529
|
+
background: #e74c3c;
|
|
2530
|
+
color: white;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
.retry-description {
|
|
2534
|
+
font-size: 0.9rem;
|
|
2535
|
+
color: #6c757d;
|
|
2536
|
+
font-style: italic;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
/* Retries Section */
|
|
2540
|
+
.retry-item {
|
|
2541
|
+
padding: 1rem;
|
|
2542
|
+
margin-bottom: 1rem;
|
|
2543
|
+
border: 1px solid #f39c12;
|
|
2544
|
+
border-radius: 4px;
|
|
2545
|
+
background: #fef9e7;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
.retry-item h4 {
|
|
2549
|
+
color: #d68910;
|
|
2550
|
+
margin-bottom: 0.5rem;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
.retry-details {
|
|
2554
|
+
display: flex;
|
|
2555
|
+
gap: 1rem;
|
|
2556
|
+
align-items: center;
|
|
2557
|
+
font-size: 0.9rem;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
.status-badge {
|
|
2561
|
+
padding: 0.25rem 0.5rem;
|
|
2562
|
+
border-radius: 4px;
|
|
2563
|
+
font-size: 0.8rem;
|
|
2564
|
+
font-weight: bold;
|
|
2565
|
+
text-transform: uppercase;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
.status-badge.passed {
|
|
2569
|
+
background: #27ae60;
|
|
2570
|
+
color: white;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
.status-badge.failed {
|
|
2574
|
+
background: #e74c3c;
|
|
2575
|
+
color: white;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
.status-badge.pending {
|
|
2579
|
+
background: #f39c12;
|
|
2580
|
+
color: white;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
/* History Chart */
|
|
2584
|
+
.history-stats {
|
|
2585
|
+
padding: 1.5rem;
|
|
2586
|
+
background: #f8f9fa;
|
|
2587
|
+
border-bottom: 1px solid #e9ecef;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
.history-stats-grid {
|
|
2591
|
+
display: grid;
|
|
2592
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
2593
|
+
gap: 1rem;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
.history-stat-item {
|
|
2597
|
+
background: white;
|
|
2598
|
+
padding: 1rem;
|
|
2599
|
+
border-radius: 6px;
|
|
2600
|
+
border-left: 4px solid #3498db;
|
|
2601
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
.history-stat-item h4 {
|
|
2605
|
+
margin: 0 0 0.5rem 0;
|
|
2606
|
+
font-size: 0.9rem;
|
|
2607
|
+
color: #7f8c8d;
|
|
2608
|
+
text-transform: uppercase;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
.history-stat-item .value {
|
|
2612
|
+
font-size: 1.5rem;
|
|
2613
|
+
font-weight: bold;
|
|
2614
|
+
color: #2c3e50;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
.history-timeline {
|
|
2618
|
+
padding: 1.5rem;
|
|
2619
|
+
background: white;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
.timeline-item {
|
|
2623
|
+
display: flex;
|
|
2624
|
+
align-items: center;
|
|
2625
|
+
padding: 0.75rem;
|
|
2626
|
+
border-left: 3px solid #3498db;
|
|
2627
|
+
margin-left: 1rem;
|
|
2628
|
+
margin-bottom: 0.5rem;
|
|
2629
|
+
background: #f8f9fa;
|
|
2630
|
+
border-radius: 0 6px 6px 0;
|
|
2631
|
+
transition: all 0.2s;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
.timeline-item:hover {
|
|
2635
|
+
background: #e9ecef;
|
|
2636
|
+
transform: translateX(4px);
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
.timeline-time {
|
|
2640
|
+
min-width: 150px;
|
|
2641
|
+
font-weight: 600;
|
|
2642
|
+
color: #2c3e50;
|
|
2643
|
+
font-family: 'Courier New', monospace;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
.timeline-result {
|
|
2647
|
+
flex: 1;
|
|
2648
|
+
display: flex;
|
|
2649
|
+
gap: 1rem;
|
|
2650
|
+
align-items: center;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
.timeline-badge {
|
|
2654
|
+
padding: 0.25rem 0.5rem;
|
|
2655
|
+
border-radius: 4px;
|
|
2656
|
+
font-size: 0.85rem;
|
|
2657
|
+
font-weight: 600;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
.timeline-badge.success {
|
|
2661
|
+
background: #d4edda;
|
|
2662
|
+
color: #155724;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
.timeline-badge.failure {
|
|
2666
|
+
background: #f8d7da;
|
|
2667
|
+
color: #721c24;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
.history-chart-container {
|
|
2671
|
+
padding: 2rem 1rem;
|
|
2672
|
+
display: flex;
|
|
2673
|
+
justify-content: center;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
#historyChart {
|
|
2677
|
+
max-width: 100%;
|
|
2678
|
+
height: auto;
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
/* Test Performance Section */
|
|
2682
|
+
.performance-container {
|
|
2683
|
+
display: grid;
|
|
2684
|
+
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
2685
|
+
gap: 2rem;
|
|
2686
|
+
padding: 1.5rem;
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
.performance-group h3 {
|
|
2690
|
+
margin: 0 0 1rem 0;
|
|
2691
|
+
color: #2c3e50;
|
|
2692
|
+
font-size: 1.1rem;
|
|
2693
|
+
padding-bottom: 0.5rem;
|
|
2694
|
+
border-bottom: 2px solid #3498db;
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
.performance-list {
|
|
2698
|
+
display: flex;
|
|
2699
|
+
flex-direction: column;
|
|
2700
|
+
gap: 0.75rem;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
.performance-item {
|
|
2704
|
+
display: flex;
|
|
2705
|
+
align-items: center;
|
|
2706
|
+
justify-content: space-between;
|
|
2707
|
+
padding: 0.75rem 1rem;
|
|
2708
|
+
background: #f8f9fa;
|
|
2709
|
+
border-radius: 6px;
|
|
2710
|
+
border-left: 4px solid #3498db;
|
|
2711
|
+
transition: all 0.2s;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
.performance-item:hover {
|
|
2715
|
+
background: #e9ecef;
|
|
2716
|
+
transform: translateX(4px);
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
.performance-item:nth-child(1) .performance-rank {
|
|
2720
|
+
background: #f39c12;
|
|
2721
|
+
color: white;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
.performance-item:nth-child(2) .performance-rank {
|
|
2725
|
+
background: #95a5a6;
|
|
2726
|
+
color: white;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
.performance-item:nth-child(3) .performance-rank {
|
|
2730
|
+
background: #cd7f32;
|
|
2731
|
+
color: white;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
.performance-rank {
|
|
2735
|
+
display: flex;
|
|
2736
|
+
align-items: center;
|
|
2737
|
+
justify-content: center;
|
|
2738
|
+
width: 28px;
|
|
2739
|
+
height: 28px;
|
|
2740
|
+
background: #3498db;
|
|
2741
|
+
color: white;
|
|
2742
|
+
border-radius: 50%;
|
|
2743
|
+
font-weight: bold;
|
|
2744
|
+
font-size: 0.9rem;
|
|
2745
|
+
margin-right: 1rem;
|
|
2746
|
+
flex-shrink: 0;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
.performance-name {
|
|
2750
|
+
flex: 1;
|
|
2751
|
+
font-weight: 500;
|
|
2752
|
+
color: #2c3e50;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
.performance-duration {
|
|
2756
|
+
font-weight: 600;
|
|
2757
|
+
color: #7f8c8d;
|
|
2758
|
+
font-family: 'Courier New', monospace;
|
|
2759
|
+
font-size: 0.9rem;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
/* Hidden items for filtering */
|
|
2763
|
+
.test-item.filtered-out {
|
|
2764
|
+
display: none !important;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
/* System Info Section */
|
|
2768
|
+
.system-info-section {
|
|
2769
|
+
background: white;
|
|
2770
|
+
margin-bottom: 2rem;
|
|
2771
|
+
border-radius: 8px;
|
|
2772
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
2773
|
+
overflow: hidden;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
.system-info-header {
|
|
2777
|
+
background: #2c3e50;
|
|
2778
|
+
color: white;
|
|
2779
|
+
padding: 1rem;
|
|
2780
|
+
cursor: pointer;
|
|
2781
|
+
display: flex;
|
|
2782
|
+
justify-content: space-between;
|
|
2783
|
+
align-items: center;
|
|
2784
|
+
transition: background-color 0.2s;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
.system-info-header:hover {
|
|
2788
|
+
background: #34495e;
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
.system-info-header h3 {
|
|
2792
|
+
margin: 0;
|
|
2793
|
+
font-size: 1.2rem;
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
.toggle-icon {
|
|
2797
|
+
font-size: 1rem;
|
|
2798
|
+
transition: transform 0.3s ease;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
.toggle-icon.rotated {
|
|
2802
|
+
transform: rotate(-180deg);
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
.system-info-content {
|
|
2806
|
+
display: none;
|
|
2807
|
+
padding: 1.5rem;
|
|
2808
|
+
background: #f8f9fa;
|
|
2809
|
+
border-top: 1px solid #e9ecef;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
.system-info-content.visible {
|
|
2813
|
+
display: block;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
.system-info-grid {
|
|
2817
|
+
display: grid;
|
|
2818
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
2819
|
+
gap: 1rem;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
.info-item {
|
|
2823
|
+
padding: 0.75rem;
|
|
2824
|
+
background: white;
|
|
2825
|
+
border-radius: 6px;
|
|
2826
|
+
border-left: 4px solid #3498db;
|
|
2827
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
.info-key {
|
|
2831
|
+
font-weight: bold;
|
|
2832
|
+
color: #2c3e50;
|
|
2833
|
+
display: inline-block;
|
|
2834
|
+
min-width: 100px;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
.info-value {
|
|
2838
|
+
color: #34495e;
|
|
2839
|
+
font-family: 'Courier New', monospace;
|
|
2840
|
+
font-size: 0.9rem;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
/* BDD/Gherkin specific styles */
|
|
2844
|
+
.bdd-test {
|
|
2845
|
+
border-left: 4px solid #8e44ad;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
.bdd-badge {
|
|
2849
|
+
background: #8e44ad;
|
|
2850
|
+
color: white;
|
|
2851
|
+
padding: 0.25rem 0.5rem;
|
|
2852
|
+
border-radius: 4px;
|
|
2853
|
+
font-size: 0.7rem;
|
|
2854
|
+
font-weight: bold;
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
.bdd-feature-section {
|
|
2858
|
+
margin-top: 1rem;
|
|
2859
|
+
padding: 1rem;
|
|
2860
|
+
background: #f8f9fa;
|
|
2861
|
+
border-left: 4px solid #8e44ad;
|
|
2862
|
+
border-radius: 4px;
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
.feature-name {
|
|
2866
|
+
font-weight: bold;
|
|
2867
|
+
font-size: 1.1rem;
|
|
2868
|
+
color: #8e44ad;
|
|
2869
|
+
margin-bottom: 0.5rem;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
.feature-description {
|
|
2873
|
+
color: #34495e;
|
|
2874
|
+
font-style: italic;
|
|
2875
|
+
margin: 0.5rem 0;
|
|
2876
|
+
padding: 0.5rem;
|
|
2877
|
+
background: white;
|
|
2878
|
+
border-radius: 4px;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
.feature-file {
|
|
2882
|
+
font-size: 0.8rem;
|
|
2883
|
+
color: #7f8c8d;
|
|
2884
|
+
margin-top: 0.5rem;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
.feature-tags {
|
|
2888
|
+
display: flex;
|
|
2889
|
+
flex-wrap: wrap;
|
|
2890
|
+
gap: 0.25rem;
|
|
2891
|
+
margin: 0.5rem 0;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
.feature-tag {
|
|
2895
|
+
background: #8e44ad;
|
|
2896
|
+
color: white;
|
|
2897
|
+
padding: 0.2rem 0.4rem;
|
|
2898
|
+
border-radius: 8px;
|
|
2899
|
+
font-size: 0.7rem;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
.bdd-steps-section {
|
|
2903
|
+
margin-top: 1rem;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
.bdd-steps-section h4 {
|
|
2907
|
+
color: #8e44ad;
|
|
2908
|
+
margin-bottom: 0.5rem;
|
|
2909
|
+
font-size: 1rem;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
.bdd-step-item {
|
|
2913
|
+
display: flex;
|
|
2914
|
+
align-items: flex-start;
|
|
2915
|
+
padding: 0.5rem 0;
|
|
2916
|
+
border-bottom: 1px solid #ecf0f1;
|
|
2917
|
+
font-family: 'Segoe UI', sans-serif;
|
|
2918
|
+
word-wrap: break-word;
|
|
2919
|
+
overflow-wrap: break-word;
|
|
2920
|
+
min-height: 2rem;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
.bdd-step-item:last-child {
|
|
2924
|
+
border-bottom: none;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
.bdd-keyword {
|
|
2928
|
+
font-weight: bold;
|
|
2929
|
+
color: #8e44ad;
|
|
2930
|
+
margin-right: 0.5rem;
|
|
2931
|
+
min-width: 60px;
|
|
2932
|
+
text-align: left;
|
|
2933
|
+
flex-shrink: 0;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
.bdd-step-text {
|
|
2937
|
+
flex: 1;
|
|
2938
|
+
color: #2c3e50;
|
|
2939
|
+
margin-right: 0.5rem;
|
|
2940
|
+
word-wrap: break-word;
|
|
2941
|
+
overflow-wrap: break-word;
|
|
2942
|
+
line-height: 1.4;
|
|
2943
|
+
min-width: 0;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
.step-comment {
|
|
2947
|
+
width: 100%;
|
|
2948
|
+
margin-top: 0.5rem;
|
|
2949
|
+
padding: 0.5rem;
|
|
2950
|
+
background: #f8f9fa;
|
|
2951
|
+
border-left: 3px solid #8e44ad;
|
|
2952
|
+
font-style: italic;
|
|
2953
|
+
color: #6c757d;
|
|
2954
|
+
word-wrap: break-word;
|
|
2955
|
+
overflow-wrap: break-word;
|
|
2956
|
+
line-height: 1.4;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
@media (max-width: 768px) {
|
|
2960
|
+
.stats-cards {
|
|
2961
|
+
flex-direction: column;
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
.test-header {
|
|
2965
|
+
flex-direction: column;
|
|
2966
|
+
align-items: stretch;
|
|
2967
|
+
gap: 0.5rem;
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
.test-feature, .test-duration {
|
|
2971
|
+
align-self: flex-start;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
`
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
function getJsScripts() {
|
|
2978
|
+
return `
|
|
2979
|
+
// Go to Top button
|
|
2980
|
+
function scrollToTop() {
|
|
2981
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
function toggleFeatureGroup(featureId) {
|
|
2985
|
+
const featureTests = document.getElementById('feature-' + featureId);
|
|
2986
|
+
const titleElement = featureTests.previousElementSibling;
|
|
2987
|
+
const icon = titleElement.querySelector('.toggle-icon');
|
|
2988
|
+
|
|
2989
|
+
if (featureTests.classList.contains('collapsed')) {
|
|
2990
|
+
featureTests.classList.remove('collapsed');
|
|
2991
|
+
icon.classList.remove('rotated');
|
|
2992
|
+
} else {
|
|
2993
|
+
featureTests.classList.add('collapsed');
|
|
2994
|
+
icon.classList.add('rotated');
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
function toggleTestDetails(testId) {
|
|
2999
|
+
const details = document.getElementById('details-' + testId);
|
|
3000
|
+
if (details.style.display === 'none' || details.style.display === '') {
|
|
3001
|
+
details.style.display = 'block';
|
|
3002
|
+
} else {
|
|
3003
|
+
details.style.display = 'none';
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
function openImageModal(src) {
|
|
3008
|
+
const modal = document.getElementById('imageModal');
|
|
3009
|
+
const modalImg = document.getElementById('modalImage');
|
|
3010
|
+
modalImg.src = src;
|
|
3011
|
+
modal.style.display = 'block';
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
function closeImageModal() {
|
|
3015
|
+
const modal = document.getElementById('imageModal');
|
|
3016
|
+
modal.style.display = 'none';
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
function toggleSystemInfo() {
|
|
3020
|
+
const content = document.getElementById('systemInfoContent');
|
|
3021
|
+
const icon = document.querySelector('.toggle-icon');
|
|
3022
|
+
|
|
3023
|
+
if (content.classList.contains('visible')) {
|
|
3024
|
+
content.classList.remove('visible');
|
|
3025
|
+
icon.classList.remove('rotated');
|
|
3026
|
+
} else {
|
|
3027
|
+
content.classList.add('visible');
|
|
3028
|
+
icon.classList.add('rotated');
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// Filter functionality
|
|
3033
|
+
function applyFilters() {
|
|
3034
|
+
const statusFilter = Array.from(document.getElementById('statusFilter').selectedOptions).map(opt => opt.value);
|
|
3035
|
+
const featureFilter = document.getElementById('featureFilter').value.toLowerCase();
|
|
3036
|
+
const tagFilter = document.getElementById('tagFilter').value.toLowerCase();
|
|
3037
|
+
const retryFilter = document.getElementById('retryFilter').value;
|
|
3038
|
+
const typeFilter = document.getElementById('typeFilter').value;
|
|
3039
|
+
|
|
3040
|
+
const testItems = document.querySelectorAll('.test-item');
|
|
3041
|
+
|
|
3042
|
+
testItems.forEach(item => {
|
|
3043
|
+
let shouldShow = true;
|
|
3044
|
+
|
|
3045
|
+
// Status filter
|
|
3046
|
+
if (statusFilter.length > 0) {
|
|
3047
|
+
const testStatus = item.dataset.status;
|
|
3048
|
+
if (!statusFilter.includes(testStatus)) {
|
|
3049
|
+
shouldShow = false;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// Feature filter
|
|
3054
|
+
if (featureFilter && shouldShow) {
|
|
3055
|
+
const feature = (item.dataset.feature || '').toLowerCase();
|
|
3056
|
+
if (!feature.includes(featureFilter)) {
|
|
3057
|
+
shouldShow = false;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
// Tag filter
|
|
3062
|
+
if (tagFilter && shouldShow) {
|
|
3063
|
+
const tags = (item.dataset.tags || '').toLowerCase();
|
|
3064
|
+
if (!tags.includes(tagFilter)) {
|
|
3065
|
+
shouldShow = false;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
// Retry filter
|
|
3070
|
+
if (retryFilter !== 'all' && shouldShow) {
|
|
3071
|
+
const retries = parseInt(item.dataset.retries || '0');
|
|
3072
|
+
if (retryFilter === 'retried' && retries === 0) {
|
|
3073
|
+
shouldShow = false;
|
|
3074
|
+
} else if (retryFilter === 'no-retries' && retries > 0) {
|
|
3075
|
+
shouldShow = false;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
// Test type filter (BDD/Gherkin vs Regular)
|
|
3080
|
+
if (typeFilter !== 'all' && shouldShow) {
|
|
3081
|
+
const testType = item.dataset.type || 'regular';
|
|
3082
|
+
if (typeFilter !== testType) {
|
|
3083
|
+
shouldShow = false;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
if (shouldShow) {
|
|
3088
|
+
item.classList.remove('filtered-out');
|
|
3089
|
+
} else {
|
|
3090
|
+
item.classList.add('filtered-out');
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
|
|
3094
|
+
updateFilteredStats();
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
function resetFilters() {
|
|
3098
|
+
document.getElementById('statusFilter').selectedIndex = -1;
|
|
3099
|
+
document.getElementById('featureFilter').value = '';
|
|
3100
|
+
document.getElementById('tagFilter').value = '';
|
|
3101
|
+
document.getElementById('retryFilter').value = 'all';
|
|
3102
|
+
document.getElementById('typeFilter').value = 'all';
|
|
3103
|
+
|
|
3104
|
+
document.querySelectorAll('.test-item').forEach(item => {
|
|
3105
|
+
item.classList.remove('filtered-out');
|
|
3106
|
+
});
|
|
3107
|
+
|
|
3108
|
+
updateFilteredStats();
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
function updateFilteredStats() {
|
|
3112
|
+
const visibleTests = document.querySelectorAll('.test-item:not(.filtered-out)');
|
|
3113
|
+
const totalVisible = visibleTests.length;
|
|
3114
|
+
|
|
3115
|
+
// Update the title to show filtered count
|
|
3116
|
+
const testsSection = document.querySelector('.tests-section h2');
|
|
3117
|
+
const totalTests = document.querySelectorAll('.test-item').length;
|
|
3118
|
+
|
|
3119
|
+
if (totalVisible !== totalTests) {
|
|
3120
|
+
testsSection.textContent = 'Test Results (' + totalVisible + ' of ' + totalTests + ' shown)';
|
|
3121
|
+
} else {
|
|
3122
|
+
testsSection.textContent = 'Test Results';
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
// Draw pie chart using canvas
|
|
3127
|
+
function drawPieChart() {
|
|
3128
|
+
const canvas = document.getElementById('statsChart');
|
|
3129
|
+
if (!canvas) return;
|
|
3130
|
+
|
|
3131
|
+
const ctx = canvas.getContext('2d');
|
|
3132
|
+
const data = window.chartData;
|
|
3133
|
+
|
|
3134
|
+
if (!data) return;
|
|
3135
|
+
|
|
3136
|
+
const centerX = canvas.width / 2;
|
|
3137
|
+
const centerY = canvas.height / 2;
|
|
3138
|
+
const radius = Math.min(centerX, centerY) - 20;
|
|
3139
|
+
|
|
3140
|
+
const total = data.passed + data.failed + data.pending;
|
|
3141
|
+
if (total === 0) {
|
|
3142
|
+
// Draw empty circle for no tests
|
|
3143
|
+
ctx.strokeStyle = '#ddd';
|
|
3144
|
+
ctx.lineWidth = 2;
|
|
3145
|
+
ctx.beginPath();
|
|
3146
|
+
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
|
3147
|
+
ctx.stroke();
|
|
3148
|
+
ctx.fillStyle = '#888';
|
|
3149
|
+
ctx.font = '16px Arial';
|
|
3150
|
+
ctx.textAlign = 'center';
|
|
3151
|
+
ctx.fillText('No Tests', centerX, centerY);
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
let currentAngle = -Math.PI / 2; // Start from top
|
|
3156
|
+
|
|
3157
|
+
// Calculate percentages
|
|
3158
|
+
const passedPercent = Math.round((data.passed / total) * 100);
|
|
3159
|
+
const failedPercent = Math.round((data.failed / total) * 100);
|
|
3160
|
+
const pendingPercent = Math.round((data.pending / total) * 100);
|
|
3161
|
+
|
|
3162
|
+
// Draw passed segment
|
|
3163
|
+
if (data.passed > 0) {
|
|
3164
|
+
const angle = (data.passed / total) * 2 * Math.PI;
|
|
3165
|
+
ctx.beginPath();
|
|
3166
|
+
ctx.moveTo(centerX, centerY);
|
|
3167
|
+
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + angle);
|
|
3168
|
+
ctx.closePath();
|
|
3169
|
+
ctx.fillStyle = '#27ae60';
|
|
3170
|
+
ctx.fill();
|
|
3171
|
+
|
|
3172
|
+
// Add percentage text on segment if significant enough
|
|
3173
|
+
if (passedPercent >= 10) {
|
|
3174
|
+
const textAngle = currentAngle + angle / 2;
|
|
3175
|
+
const textRadius = radius * 0.7;
|
|
3176
|
+
const textX = centerX + Math.cos(textAngle) * textRadius;
|
|
3177
|
+
const textY = centerY + Math.sin(textAngle) * textRadius;
|
|
3178
|
+
|
|
3179
|
+
ctx.fillStyle = '#fff';
|
|
3180
|
+
ctx.font = 'bold 14px Arial';
|
|
3181
|
+
ctx.textAlign = 'center';
|
|
3182
|
+
ctx.textBaseline = 'middle';
|
|
3183
|
+
ctx.fillText(passedPercent + '%', textX, textY);
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
currentAngle += angle;
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
// Draw failed segment
|
|
3190
|
+
if (data.failed > 0) {
|
|
3191
|
+
const angle = (data.failed / total) * 2 * Math.PI;
|
|
3192
|
+
ctx.beginPath();
|
|
3193
|
+
ctx.moveTo(centerX, centerY);
|
|
3194
|
+
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + angle);
|
|
3195
|
+
ctx.closePath();
|
|
3196
|
+
ctx.fillStyle = '#e74c3c';
|
|
3197
|
+
ctx.fill();
|
|
3198
|
+
|
|
3199
|
+
// Add percentage text on segment if significant enough
|
|
3200
|
+
if (failedPercent >= 10) {
|
|
3201
|
+
const textAngle = currentAngle + angle / 2;
|
|
3202
|
+
const textRadius = radius * 0.7;
|
|
3203
|
+
const textX = centerX + Math.cos(textAngle) * textRadius;
|
|
3204
|
+
const textY = centerY + Math.sin(textAngle) * textRadius;
|
|
3205
|
+
|
|
3206
|
+
ctx.fillStyle = '#fff';
|
|
3207
|
+
ctx.font = 'bold 14px Arial';
|
|
3208
|
+
ctx.textAlign = 'center';
|
|
3209
|
+
ctx.textBaseline = 'middle';
|
|
3210
|
+
ctx.fillText(failedPercent + '%', textX, textY);
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
currentAngle += angle;
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
// Draw pending segment
|
|
3217
|
+
if (data.pending > 0) {
|
|
3218
|
+
const angle = (data.pending / total) * 2 * Math.PI;
|
|
3219
|
+
ctx.beginPath();
|
|
3220
|
+
ctx.moveTo(centerX, centerY);
|
|
3221
|
+
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + angle);
|
|
3222
|
+
ctx.closePath();
|
|
3223
|
+
ctx.fillStyle = '#f39c12';
|
|
3224
|
+
ctx.fill();
|
|
3225
|
+
|
|
3226
|
+
// Add percentage text on segment if significant enough
|
|
3227
|
+
if (pendingPercent >= 10) {
|
|
3228
|
+
const textAngle = currentAngle + angle / 2;
|
|
3229
|
+
const textRadius = radius * 0.7;
|
|
3230
|
+
const textX = centerX + Math.cos(textAngle) * textRadius;
|
|
3231
|
+
const textY = centerY + Math.sin(textAngle) * textRadius;
|
|
3232
|
+
|
|
3233
|
+
ctx.fillStyle = '#fff';
|
|
3234
|
+
ctx.font = 'bold 14px Arial';
|
|
3235
|
+
ctx.textAlign = 'center';
|
|
3236
|
+
ctx.textBaseline = 'middle';
|
|
3237
|
+
ctx.fillText(pendingPercent + '%', textX, textY);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
// Add legend with percentages
|
|
3242
|
+
const legendY = centerY + radius + 40;
|
|
3243
|
+
ctx.font = '14px Arial';
|
|
3244
|
+
ctx.textAlign = 'left';
|
|
3245
|
+
ctx.textBaseline = 'alphabetic';
|
|
3246
|
+
|
|
3247
|
+
let legendX = centerX - 150;
|
|
3248
|
+
|
|
3249
|
+
// Passed legend
|
|
3250
|
+
ctx.fillStyle = '#27ae60';
|
|
3251
|
+
ctx.fillRect(legendX, legendY, 15, 15);
|
|
3252
|
+
ctx.fillStyle = '#333';
|
|
3253
|
+
ctx.fillText('Passed (' + data.passed + ' - ' + passedPercent + '%)', legendX + 20, legendY + 12);
|
|
3254
|
+
|
|
3255
|
+
// Failed legend
|
|
3256
|
+
legendX += 130;
|
|
3257
|
+
ctx.fillStyle = '#e74c3c';
|
|
3258
|
+
ctx.fillRect(legendX, legendY, 15, 15);
|
|
3259
|
+
ctx.fillStyle = '#333';
|
|
3260
|
+
ctx.fillText('Failed (' + data.failed + ' - ' + failedPercent + '%)', legendX + 20, legendY + 12);
|
|
3261
|
+
|
|
3262
|
+
// Pending legend
|
|
3263
|
+
if (data.pending > 0) {
|
|
3264
|
+
legendX += 120;
|
|
3265
|
+
ctx.fillStyle = '#f39c12';
|
|
3266
|
+
ctx.fillRect(legendX, legendY, 15, 15);
|
|
3267
|
+
ctx.fillStyle = '#333';
|
|
3268
|
+
ctx.fillText('Pending (' + data.pending + ' - ' + pendingPercent + '%)', legendX + 20, legendY + 12);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
// Draw history chart
|
|
3273
|
+
function drawHistoryChart() {
|
|
3274
|
+
const canvas = document.getElementById('historyChart');
|
|
3275
|
+
|
|
3276
|
+
if (!canvas || !window.testData || !window.testData.history || window.testData.history.length === 0) {
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
const ctx = canvas.getContext('2d');
|
|
3281
|
+
const history = window.testData.history.slice().reverse(); // Most recent last
|
|
3282
|
+
console.log('History chart - Total data points:', window.testData.history.length);
|
|
3283
|
+
console.log('History chart - Processing points:', history.length);
|
|
3284
|
+
console.log('History chart - Raw history data:', window.testData.history);
|
|
3285
|
+
console.log('History chart - Reversed history:', history);
|
|
3286
|
+
|
|
3287
|
+
const padding = 60;
|
|
3288
|
+
const bottomPadding = 80; // Extra space for timestamps
|
|
3289
|
+
const chartWidth = canvas.width - 2 * padding;
|
|
3290
|
+
const chartHeight = canvas.height - padding - bottomPadding;
|
|
3291
|
+
|
|
3292
|
+
// Clear canvas
|
|
3293
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
3294
|
+
|
|
3295
|
+
// Calculate success rates and max values
|
|
3296
|
+
const dataPoints = history.map((run, index) => {
|
|
3297
|
+
const total = run.stats.tests || 0;
|
|
3298
|
+
const passed = run.stats.passes || 0;
|
|
3299
|
+
const failed = run.stats.failures || 0;
|
|
3300
|
+
const successRate = total > 0 ? (passed / total) * 100 : 0;
|
|
3301
|
+
const timestamp = new Date(run.timestamp);
|
|
3302
|
+
|
|
3303
|
+
return {
|
|
3304
|
+
index,
|
|
3305
|
+
timestamp,
|
|
3306
|
+
total,
|
|
3307
|
+
passed,
|
|
3308
|
+
failed,
|
|
3309
|
+
successRate,
|
|
3310
|
+
duration: run.duration || 0,
|
|
3311
|
+
retries: run.retries || 0
|
|
3312
|
+
};
|
|
3313
|
+
});
|
|
3314
|
+
|
|
3315
|
+
console.log('History chart - Data points created:', dataPoints.length);
|
|
3316
|
+
console.log('History chart - Data points:', dataPoints);
|
|
3317
|
+
|
|
3318
|
+
const maxTests = Math.max(...dataPoints.map(d => d.total));
|
|
3319
|
+
const maxSuccessRate = 100;
|
|
3320
|
+
|
|
3321
|
+
if (maxTests === 0) return;
|
|
3322
|
+
|
|
3323
|
+
// Draw background
|
|
3324
|
+
ctx.fillStyle = '#fafafa';
|
|
3325
|
+
ctx.fillRect(padding, padding, chartWidth, chartHeight);
|
|
3326
|
+
|
|
3327
|
+
// Draw axes
|
|
3328
|
+
ctx.strokeStyle = '#333';
|
|
3329
|
+
ctx.lineWidth = 2;
|
|
3330
|
+
ctx.beginPath();
|
|
3331
|
+
ctx.moveTo(padding, padding);
|
|
3332
|
+
ctx.lineTo(padding, padding + chartHeight);
|
|
3333
|
+
ctx.lineTo(padding + chartWidth, padding + chartHeight);
|
|
3334
|
+
ctx.stroke();
|
|
3335
|
+
|
|
3336
|
+
// Draw grid lines
|
|
3337
|
+
ctx.strokeStyle = '#e0e0e0';
|
|
3338
|
+
ctx.lineWidth = 1;
|
|
3339
|
+
for (let i = 1; i <= 4; i++) {
|
|
3340
|
+
const y = padding + (chartHeight * i / 4);
|
|
3341
|
+
ctx.beginPath();
|
|
3342
|
+
ctx.moveTo(padding, y);
|
|
3343
|
+
ctx.lineTo(padding + chartWidth, y);
|
|
3344
|
+
ctx.stroke();
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// Calculate positions
|
|
3348
|
+
const stepX = dataPoints.length > 1 ? chartWidth / (dataPoints.length - 1) : chartWidth / 2;
|
|
3349
|
+
|
|
3350
|
+
// Draw success rate area chart
|
|
3351
|
+
ctx.fillStyle = 'rgba(39, 174, 96, 0.1)';
|
|
3352
|
+
ctx.strokeStyle = '#27ae60';
|
|
3353
|
+
ctx.lineWidth = 3;
|
|
3354
|
+
ctx.beginPath();
|
|
3355
|
+
|
|
3356
|
+
dataPoints.forEach((point, index) => {
|
|
3357
|
+
const x = dataPoints.length === 1 ? padding + chartWidth / 2 : padding + (index * stepX);
|
|
3358
|
+
const y = padding + chartHeight - (point.successRate / maxSuccessRate) * chartHeight;
|
|
3359
|
+
|
|
3360
|
+
if (index === 0) {
|
|
3361
|
+
ctx.moveTo(x, padding + chartHeight);
|
|
3362
|
+
ctx.lineTo(x, y);
|
|
3363
|
+
} else {
|
|
3364
|
+
ctx.lineTo(x, y);
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
point.x = x;
|
|
3368
|
+
point.y = y;
|
|
3369
|
+
});
|
|
3370
|
+
|
|
3371
|
+
// Close the area
|
|
3372
|
+
if (dataPoints.length > 0) {
|
|
3373
|
+
const lastPoint = dataPoints[dataPoints.length - 1];
|
|
3374
|
+
ctx.lineTo(lastPoint.x, padding + chartHeight);
|
|
3375
|
+
ctx.closePath();
|
|
3376
|
+
ctx.fill();
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
// Draw success rate line
|
|
3380
|
+
ctx.strokeStyle = '#27ae60';
|
|
3381
|
+
ctx.lineWidth = 3;
|
|
3382
|
+
ctx.beginPath();
|
|
3383
|
+
dataPoints.forEach((point, index) => {
|
|
3384
|
+
if (index === 0) {
|
|
3385
|
+
ctx.moveTo(point.x, point.y);
|
|
3386
|
+
} else {
|
|
3387
|
+
ctx.lineTo(point.x, point.y);
|
|
3388
|
+
}
|
|
3389
|
+
});
|
|
3390
|
+
ctx.stroke();
|
|
3391
|
+
|
|
3392
|
+
// Draw data points with enhanced styling
|
|
3393
|
+
dataPoints.forEach(point => {
|
|
3394
|
+
// Outer ring based on status
|
|
3395
|
+
const ringColor = point.failed > 0 ? '#e74c3c' : '#27ae60';
|
|
3396
|
+
ctx.strokeStyle = ringColor;
|
|
3397
|
+
ctx.lineWidth = 3;
|
|
3398
|
+
ctx.beginPath();
|
|
3399
|
+
ctx.arc(point.x, point.y, 8, 0, 2 * Math.PI);
|
|
3400
|
+
ctx.stroke();
|
|
3401
|
+
|
|
3402
|
+
// Inner circle
|
|
3403
|
+
ctx.fillStyle = point.failed > 0 ? '#e74c3c' : '#27ae60';
|
|
3404
|
+
ctx.beginPath();
|
|
3405
|
+
ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI);
|
|
3406
|
+
ctx.fill();
|
|
3407
|
+
|
|
3408
|
+
// White center dot
|
|
3409
|
+
ctx.fillStyle = '#fff';
|
|
3410
|
+
ctx.beginPath();
|
|
3411
|
+
ctx.arc(point.x, point.y, 2, 0, 2 * Math.PI);
|
|
3412
|
+
ctx.fill();
|
|
3413
|
+
});
|
|
3414
|
+
|
|
3415
|
+
// Y-axis labels (Success Rate %)
|
|
3416
|
+
ctx.fillStyle = '#666';
|
|
3417
|
+
ctx.font = '11px Arial';
|
|
3418
|
+
ctx.textAlign = 'right';
|
|
3419
|
+
for (let i = 0; i <= 4; i++) {
|
|
3420
|
+
const value = Math.round((maxSuccessRate * i) / 4);
|
|
3421
|
+
const y = padding + chartHeight - (chartHeight * i / 4);
|
|
3422
|
+
ctx.fillText(value + '%', padding - 10, y + 4);
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
// X-axis labels (Timestamps)
|
|
3426
|
+
ctx.textAlign = 'center';
|
|
3427
|
+
ctx.font = '10px Arial';
|
|
3428
|
+
dataPoints.forEach((point, index) => {
|
|
3429
|
+
const timeStr = point.timestamp.toLocaleTimeString('en-US', {
|
|
3430
|
+
hour: '2-digit',
|
|
3431
|
+
minute: '2-digit',
|
|
3432
|
+
hour12: false
|
|
3433
|
+
});
|
|
3434
|
+
const dateStr = point.timestamp.toLocaleDateString('en-US', {
|
|
3435
|
+
month: 'short',
|
|
3436
|
+
day: 'numeric'
|
|
3437
|
+
});
|
|
3438
|
+
|
|
3439
|
+
console.log('Drawing label ' + index + ': ' + timeStr + ' at x=' + point.x);
|
|
3440
|
+
ctx.fillText(timeStr, point.x, padding + chartHeight + 15);
|
|
3441
|
+
ctx.fillText(dateStr, point.x, padding + chartHeight + 30);
|
|
3442
|
+
});
|
|
3443
|
+
|
|
3444
|
+
// Enhanced legend with statistics
|
|
3445
|
+
const legendY = 25;
|
|
3446
|
+
ctx.font = '12px Arial';
|
|
3447
|
+
ctx.textAlign = 'left';
|
|
3448
|
+
|
|
3449
|
+
// Success rate legend
|
|
3450
|
+
ctx.fillStyle = '#27ae60';
|
|
3451
|
+
ctx.fillRect(padding + 20, legendY, 15, 15);
|
|
3452
|
+
ctx.fillStyle = '#333';
|
|
3453
|
+
ctx.fillText('Success Rate', padding + 40, legendY + 12);
|
|
3454
|
+
|
|
3455
|
+
// Current stats
|
|
3456
|
+
if (dataPoints.length > 0) {
|
|
3457
|
+
const latest = dataPoints[dataPoints.length - 1];
|
|
3458
|
+
const trend = dataPoints.length > 1 ?
|
|
3459
|
+
(latest.successRate - dataPoints[dataPoints.length - 2].successRate) : 0;
|
|
3460
|
+
const trendIcon = trend > 0 ? '↗' : trend < 0 ? '↘' : '→';
|
|
3461
|
+
const trendColor = trend > 0 ? '#27ae60' : trend < 0 ? '#e74c3c' : '#666';
|
|
3462
|
+
|
|
3463
|
+
ctx.fillStyle = '#666';
|
|
3464
|
+
ctx.fillText('Latest: ' + latest.successRate.toFixed(1) + '%', padding + 150, legendY + 12);
|
|
3465
|
+
|
|
3466
|
+
ctx.fillStyle = trendColor;
|
|
3467
|
+
ctx.fillText(trendIcon + ' ' + Math.abs(trend).toFixed(1) + '%', padding + 240, legendY + 12);
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// Chart title
|
|
3471
|
+
ctx.fillStyle = '#333';
|
|
3472
|
+
ctx.font = 'bold 14px Arial';
|
|
3473
|
+
ctx.textAlign = 'center';
|
|
3474
|
+
ctx.fillText('Test Success Rate History', canvas.width / 2, 20);
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
// Initialize charts and filters
|
|
3478
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
3479
|
+
|
|
3480
|
+
// Draw charts
|
|
3481
|
+
drawPieChart();
|
|
3482
|
+
drawHistoryChart();
|
|
3483
|
+
renderTestPerformance();
|
|
3484
|
+
renderHistoryTimeline();
|
|
3485
|
+
|
|
3486
|
+
// Add Go to Top button
|
|
3487
|
+
const goTopBtn = document.createElement('button');
|
|
3488
|
+
goTopBtn.innerText = '↑ Top';
|
|
3489
|
+
goTopBtn.id = 'goTopBtn';
|
|
3490
|
+
goTopBtn.style.position = 'fixed';
|
|
3491
|
+
goTopBtn.style.bottom = '30px';
|
|
3492
|
+
goTopBtn.style.right = '30px';
|
|
3493
|
+
goTopBtn.style.zIndex = '9999';
|
|
3494
|
+
goTopBtn.style.padding = '12px 18px';
|
|
3495
|
+
goTopBtn.style.borderRadius = '50%';
|
|
3496
|
+
goTopBtn.style.background = '#27ae60';
|
|
3497
|
+
goTopBtn.style.color = '#fff';
|
|
3498
|
+
goTopBtn.style.fontSize = '20px';
|
|
3499
|
+
goTopBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
|
|
3500
|
+
goTopBtn.style.cursor = 'pointer';
|
|
3501
|
+
goTopBtn.onclick = scrollToTop;
|
|
3502
|
+
document.body.appendChild(goTopBtn);
|
|
3503
|
+
|
|
3504
|
+
// Set up filter event listeners
|
|
3505
|
+
document.getElementById('statusFilter').addEventListener('change', applyFilters);
|
|
3506
|
+
document.getElementById('featureFilter').addEventListener('input', applyFilters);
|
|
3507
|
+
document.getElementById('tagFilter').addEventListener('input', applyFilters);
|
|
3508
|
+
document.getElementById('retryFilter').addEventListener('change', applyFilters);
|
|
3509
|
+
document.getElementById('typeFilter').addEventListener('change', applyFilters);
|
|
3510
|
+
});
|
|
3511
|
+
|
|
3512
|
+
// Render test performance analysis
|
|
3513
|
+
function renderTestPerformance() {
|
|
3514
|
+
const tests = Array.from(document.querySelectorAll('.test-item'));
|
|
3515
|
+
const testsWithDuration = tests.map(testEl => {
|
|
3516
|
+
const title = testEl.querySelector('.test-title')?.textContent || 'Unknown';
|
|
3517
|
+
const durationText = testEl.querySelector('.test-duration')?.textContent || '0ms';
|
|
3518
|
+
const durationMs = parseDuration(durationText);
|
|
3519
|
+
const status = testEl.dataset.status;
|
|
3520
|
+
return { title, duration: durationMs, durationText, status };
|
|
3521
|
+
}); // Don't filter out 0ms tests
|
|
3522
|
+
|
|
3523
|
+
// Sort by duration
|
|
3524
|
+
const longest = [...testsWithDuration].sort((a, b) => b.duration - a.duration).slice(0, 5);
|
|
3525
|
+
const fastest = [...testsWithDuration].sort((a, b) => a.duration - b.duration).slice(0, 5);
|
|
3526
|
+
|
|
3527
|
+
// Render longest tests
|
|
3528
|
+
const longestContainer = document.getElementById('longestTests');
|
|
3529
|
+
if (longestContainer && longest.length > 0) {
|
|
3530
|
+
longestContainer.innerHTML = longest.map((test, index) => \`
|
|
3531
|
+
<div class="performance-item">
|
|
3532
|
+
<span class="performance-rank">\${index + 1}</span>
|
|
3533
|
+
<span class="performance-name" title="\${test.title}">\${test.title.length > 60 ? test.title.substring(0, 60) + '...' : test.title}</span>
|
|
3534
|
+
<span class="performance-duration">\${test.durationText}</span>
|
|
3535
|
+
</div>
|
|
3536
|
+
\`).join('');
|
|
3537
|
+
} else if (longestContainer) {
|
|
3538
|
+
longestContainer.innerHTML = '<p style="color: #7f8c8d; padding: 1rem;">No test data available</p>';
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
// Render fastest tests
|
|
3542
|
+
const fastestContainer = document.getElementById('fastestTests');
|
|
3543
|
+
if (fastestContainer && fastest.length > 0) {
|
|
3544
|
+
fastestContainer.innerHTML = fastest.map((test, index) => \`
|
|
3545
|
+
<div class="performance-item">
|
|
3546
|
+
<span class="performance-rank">\${index + 1}</span>
|
|
3547
|
+
<span class="performance-name" title="\${test.title}">\${test.title.length > 60 ? test.title.substring(0, 60) + '...' : test.title}</span>
|
|
3548
|
+
<span class="performance-duration">\${test.durationText}</span>
|
|
3549
|
+
</div>
|
|
3550
|
+
\`).join('');
|
|
3551
|
+
} else if (fastestContainer) {
|
|
3552
|
+
fastestContainer.innerHTML = '<p style="color: #7f8c8d; padding: 1rem;">No test data available</p>';
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// Render history timeline
|
|
3557
|
+
function renderHistoryTimeline() {
|
|
3558
|
+
if (!window.testData || !window.testData.history || window.testData.history.length === 0) {
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
const history = window.testData.history.slice().reverse(); // Most recent last
|
|
3563
|
+
|
|
3564
|
+
// Render stats
|
|
3565
|
+
const statsContainer = document.getElementById('historyStats');
|
|
3566
|
+
if (statsContainer) {
|
|
3567
|
+
const totalRuns = history.length;
|
|
3568
|
+
const avgDuration = history.reduce((sum, run) => sum + (run.duration || 0), 0) / totalRuns;
|
|
3569
|
+
const avgTests = Math.round(history.reduce((sum, run) => sum + (run.stats.tests || 0), 0) / totalRuns);
|
|
3570
|
+
const avgPassRate = history.reduce((sum, run) => {
|
|
3571
|
+
const total = run.stats.tests || 0;
|
|
3572
|
+
const passed = run.stats.passes || 0;
|
|
3573
|
+
return sum + (total > 0 ? (passed / total) * 100 : 0);
|
|
3574
|
+
}, 0) / totalRuns;
|
|
3575
|
+
|
|
3576
|
+
statsContainer.innerHTML = \`
|
|
3577
|
+
<div class="history-stats-grid">
|
|
3578
|
+
<div class="history-stat-item">
|
|
3579
|
+
<h4>Total Runs</h4>
|
|
3580
|
+
<div class="value">\${totalRuns}</div>
|
|
3581
|
+
</div>
|
|
3582
|
+
<div class="history-stat-item">
|
|
3583
|
+
<h4>Avg Duration</h4>
|
|
3584
|
+
<div class="value">\${formatDuration(avgDuration)}</div>
|
|
3585
|
+
</div>
|
|
3586
|
+
<div class="history-stat-item">
|
|
3587
|
+
<h4>Avg Tests</h4>
|
|
3588
|
+
<div class="value">\${avgTests}</div>
|
|
3589
|
+
</div>
|
|
3590
|
+
<div class="history-stat-item">
|
|
3591
|
+
<h4>Avg Pass Rate</h4>
|
|
3592
|
+
<div class="value">\${avgPassRate.toFixed(1)}%</div>
|
|
3593
|
+
</div>
|
|
3594
|
+
</div>
|
|
3595
|
+
\`;
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
// Render timeline
|
|
3599
|
+
const timelineContainer = document.getElementById('historyTimeline');
|
|
3600
|
+
if (timelineContainer) {
|
|
3601
|
+
const recentHistory = history.slice(-10).reverse(); // Last 10 runs, most recent first
|
|
3602
|
+
timelineContainer.innerHTML = '<h3 style="margin: 0 0 1rem 0; color: #2c3e50;">Recent Execution Timeline</h3>' +
|
|
3603
|
+
recentHistory.map(run => {
|
|
3604
|
+
const timestamp = new Date(run.timestamp);
|
|
3605
|
+
const timeStr = timestamp.toLocaleString();
|
|
3606
|
+
const total = run.stats.tests || 0;
|
|
3607
|
+
const passed = run.stats.passes || 0;
|
|
3608
|
+
const failed = run.stats.failures || 0;
|
|
3609
|
+
const badgeClass = failed > 0 ? 'failure' : 'success';
|
|
3610
|
+
const badgeText = failed > 0 ? \`\${failed} Failed\` : \`All Passed\`;
|
|
3611
|
+
|
|
3612
|
+
return \`
|
|
3613
|
+
<div class="timeline-item">
|
|
3614
|
+
<div class="timeline-time">\${timeStr}</div>
|
|
3615
|
+
<div class="timeline-result">
|
|
3616
|
+
<span class="timeline-badge \${badgeClass}">\${badgeText}</span>
|
|
3617
|
+
<span>\${passed}/\${total} passed</span>
|
|
3618
|
+
<span>·</span>
|
|
3619
|
+
<span>\${formatDuration(run.duration || 0)}</span>
|
|
3620
|
+
</div>
|
|
3621
|
+
</div>
|
|
3622
|
+
\`;
|
|
3623
|
+
}).join('');
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
// Helper to parse duration text to milliseconds
|
|
3628
|
+
function parseDuration(durationText) {
|
|
3629
|
+
if (!durationText) return 0;
|
|
3630
|
+
const match = durationText.match(/(\\d+(?:\\.\\d+)?)(ms|s|m)/);
|
|
3631
|
+
if (!match) return 0;
|
|
3632
|
+
const value = parseFloat(match[1]);
|
|
3633
|
+
const unit = match[2];
|
|
3634
|
+
if (unit === 'ms') return value;
|
|
3635
|
+
if (unit === 's') return value * 1000;
|
|
3636
|
+
if (unit === 'm') return value * 60000;
|
|
3637
|
+
return 0;
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
// Helper to format duration
|
|
3641
|
+
function formatDuration(ms) {
|
|
3642
|
+
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
3643
|
+
if (ms < 60000) return (ms / 1000).toFixed(2) + 's';
|
|
3644
|
+
return (ms / 60000).toFixed(2) + 'm';
|
|
3645
|
+
}
|
|
3646
|
+
`
|
|
3647
|
+
}
|
|
3648
|
+
}
|