codeceptjs 3.7.6-beta.4 → 4.0.0-beta.10.esm-aria
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/bin/codecept.js +51 -53
- package/bin/test-server.js +14 -3
- package/docs/webapi/click.mustache +5 -1
- package/lib/actor.js +15 -11
- package/lib/ai.js +72 -107
- package/lib/assert/empty.js +9 -8
- package/lib/assert/equal.js +15 -17
- package/lib/assert/error.js +2 -2
- package/lib/assert/include.js +9 -11
- package/lib/assert/throws.js +1 -1
- package/lib/assert/truth.js +8 -5
- package/lib/assert.js +18 -18
- package/lib/codecept.js +102 -75
- package/lib/colorUtils.js +48 -50
- package/lib/command/check.js +32 -27
- package/lib/command/configMigrate.js +11 -10
- package/lib/command/definitions.js +16 -10
- package/lib/command/dryRun.js +16 -16
- package/lib/command/generate.js +62 -27
- package/lib/command/gherkin/init.js +36 -38
- package/lib/command/gherkin/snippets.js +14 -14
- package/lib/command/gherkin/steps.js +21 -18
- package/lib/command/info.js +8 -8
- package/lib/command/init.js +36 -29
- package/lib/command/interactive.js +11 -10
- package/lib/command/list.js +10 -9
- package/lib/command/run-multiple/chunk.js +5 -5
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +3 -3
- package/lib/command/run-multiple.js +16 -13
- package/lib/command/run-rerun.js +6 -7
- package/lib/command/run-workers.js +24 -9
- package/lib/command/run.js +23 -8
- package/lib/command/utils.js +20 -18
- package/lib/command/workers/runTests.js +197 -114
- package/lib/config.js +124 -51
- package/lib/container.js +438 -87
- package/lib/data/context.js +6 -5
- package/lib/data/dataScenarioConfig.js +1 -1
- package/lib/data/dataTableArgument.js +1 -1
- package/lib/data/table.js +1 -1
- package/lib/effects.js +94 -10
- package/lib/element/WebElement.js +2 -2
- package/lib/els.js +11 -9
- package/lib/event.js +11 -10
- package/lib/globals.js +141 -0
- package/lib/heal.js +12 -12
- package/lib/helper/AI.js +11 -11
- package/lib/helper/ApiDataFactory.js +50 -19
- package/lib/helper/Appium.js +19 -27
- package/lib/helper/FileSystem.js +32 -12
- package/lib/helper/GraphQL.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +25 -29
- package/lib/helper/Mochawesome.js +7 -4
- package/lib/helper/Playwright.js +902 -164
- package/lib/helper/Puppeteer.js +383 -76
- package/lib/helper/REST.js +29 -12
- package/lib/helper/WebDriver.js +268 -61
- package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
- 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 +18 -9
- package/lib/helper/extras/PlaywrightRestartOpts.js +34 -23
- package/lib/helper/extras/Popup.js +1 -1
- package/lib/helper/extras/React.js +29 -30
- package/lib/helper/network/actions.js +29 -44
- package/lib/helper/network/utils.js +76 -83
- package/lib/helper/scripts/blurElement.js +6 -6
- package/lib/helper/scripts/focusElement.js +6 -6
- package/lib/helper/scripts/highlightElement.js +9 -9
- package/lib/helper/scripts/isElementClickable.js +34 -34
- package/lib/helper.js +2 -1
- package/lib/history.js +23 -20
- package/lib/hooks.js +10 -10
- package/lib/html.js +90 -100
- package/lib/index.js +48 -21
- package/lib/listener/config.js +19 -12
- package/lib/listener/emptyRun.js +6 -7
- package/lib/listener/enhancedGlobalRetry.js +6 -6
- package/lib/listener/exit.js +4 -3
- package/lib/listener/globalRetry.js +5 -5
- package/lib/listener/globalTimeout.js +30 -14
- package/lib/listener/helpers.js +39 -14
- package/lib/listener/mocha.js +3 -4
- package/lib/listener/result.js +4 -5
- package/lib/listener/retryEnhancer.js +3 -3
- package/lib/listener/steps.js +8 -7
- package/lib/listener/store.js +3 -3
- package/lib/locator.js +213 -192
- package/lib/mocha/asyncWrapper.js +105 -62
- package/lib/mocha/bdd.js +99 -13
- package/lib/mocha/cli.js +59 -26
- package/lib/mocha/factory.js +78 -19
- package/lib/mocha/featureConfig.js +1 -1
- package/lib/mocha/gherkin.js +56 -24
- package/lib/mocha/hooks.js +12 -3
- package/lib/mocha/index.js +13 -4
- package/lib/mocha/inject.js +22 -5
- package/lib/mocha/scenarioConfig.js +2 -2
- package/lib/mocha/suite.js +9 -2
- package/lib/mocha/test.js +10 -7
- package/lib/mocha/ui.js +28 -18
- package/lib/output.js +10 -8
- package/lib/parser.js +44 -44
- package/lib/pause.js +15 -16
- package/lib/plugin/analyze.js +19 -12
- package/lib/plugin/auth.js +20 -21
- package/lib/plugin/autoDelay.js +12 -8
- package/lib/plugin/coverage.js +28 -11
- package/lib/plugin/customLocator.js +3 -3
- package/lib/plugin/customReporter.js +3 -2
- package/lib/plugin/enhancedRetryFailedStep.js +6 -6
- package/lib/plugin/heal.js +14 -9
- package/lib/plugin/htmlReporter.js +724 -99
- package/lib/plugin/pageInfo.js +10 -10
- package/lib/plugin/pauseOnFail.js +4 -3
- package/lib/plugin/retryFailedStep.js +48 -5
- package/lib/plugin/screenshotOnFail.js +75 -37
- package/lib/plugin/stepByStepReport.js +14 -14
- package/lib/plugin/stepTimeout.js +4 -3
- package/lib/plugin/subtitles.js +6 -5
- package/lib/recorder.js +33 -14
- package/lib/rerun.js +69 -26
- package/lib/result.js +4 -4
- package/lib/retryCoordinator.js +2 -2
- package/lib/secret.js +18 -17
- package/lib/session.js +95 -89
- package/lib/step/base.js +7 -7
- package/lib/step/comment.js +2 -2
- package/lib/step/config.js +1 -1
- package/lib/step/func.js +3 -3
- package/lib/step/helper.js +3 -3
- package/lib/step/meta.js +5 -5
- package/lib/step/record.js +11 -11
- package/lib/step/retry.js +3 -3
- package/lib/step/section.js +3 -3
- package/lib/step.js +7 -10
- package/lib/steps.js +9 -5
- package/lib/store.js +1 -1
- package/lib/template/heal.js +1 -1
- 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 +17 -6
- package/lib/timeout.js +1 -7
- package/lib/transform.js +8 -8
- package/lib/translation.js +32 -18
- package/lib/utils/mask_data.js +4 -10
- package/lib/utils.js +66 -64
- package/lib/workerStorage.js +17 -17
- package/lib/workers.js +214 -84
- package/package.json +41 -37
- package/translations/de-DE.js +2 -2
- package/translations/fr-FR.js +2 -2
- package/translations/index.js +23 -10
- package/translations/it-IT.js +2 -2
- package/translations/ja-JP.js +2 -2
- package/translations/nl-NL.js +2 -2
- package/translations/pl-PL.js +2 -2
- package/translations/pt-BR.js +2 -2
- package/translations/ru-RU.js +2 -2
- package/translations/utils.js +4 -3
- package/translations/zh-CN.js +2 -2
- package/translations/zh-TW.js +2 -2
- package/typings/index.d.ts +5 -3
- package/typings/promiseBasedTypes.d.ts +4 -0
- package/typings/types.d.ts +4 -0
- package/lib/helper/Nightmare.js +0 -1486
- package/lib/helper/Protractor.js +0 -1840
- package/lib/helper/TestCafe.js +0 -1391
- package/lib/helper/clientscripts/nightmare.js +0 -213
- package/lib/helper/testcafe/testControllerHolder.js +0 -42
- package/lib/helper/testcafe/testcafe-utils.js +0 -61
- package/lib/plugin/allure.js +0 -15
- package/lib/plugin/autoLogin.js +0 -5
- package/lib/plugin/commentStep.js +0 -141
- package/lib/plugin/eachElement.js +0 -127
- package/lib/plugin/fakerTransform.js +0 -49
- package/lib/plugin/retryTo.js +0 -16
- package/lib/plugin/selenoid.js +0 -364
- package/lib/plugin/standardActingHelpers.js +0 -6
- package/lib/plugin/tryTo.js +0 -16
- package/lib/plugin/wdio.js +0 -247
- package/lib/within.js +0 -90
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
// TypeScript: Import Node.js types for process, fs, path, etc.
|
|
3
3
|
/// <reference types="node" />
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
16
|
|
|
17
17
|
const defaultConfig = {
|
|
18
18
|
output: typeof global !== 'undefined' && global.output_dir ? global.output_dir : './output',
|
|
@@ -62,7 +62,7 @@ const defaultConfig = {
|
|
|
62
62
|
* }
|
|
63
63
|
* ```
|
|
64
64
|
*/
|
|
65
|
-
|
|
65
|
+
export default function (config) {
|
|
66
66
|
const options = { ...defaultConfig, ...config }
|
|
67
67
|
/**
|
|
68
68
|
* TypeScript: Explicitly type reportData arrays as any[] to avoid 'never' errors
|
|
@@ -183,12 +183,21 @@ module.exports = function (config) {
|
|
|
183
183
|
if (hook.htmlReporterStartTime) {
|
|
184
184
|
hook.duration = Date.now() - hook.htmlReporterStartTime
|
|
185
185
|
}
|
|
186
|
+
// Enhanced hook info: include type, name, location, error, and context
|
|
186
187
|
const hookInfo = {
|
|
187
188
|
title: hook.title,
|
|
188
189
|
type: hook.type || 'unknown', // before, after, beforeSuite, afterSuite
|
|
189
190
|
status: hook.err ? 'failed' : 'passed',
|
|
190
191
|
duration: hook.duration || 0,
|
|
191
|
-
error: hook.err ? hook.err.message : null,
|
|
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,
|
|
192
201
|
}
|
|
193
202
|
currentTestHooks.push(hookInfo)
|
|
194
203
|
reportData.hooks.push(hookInfo)
|
|
@@ -212,6 +221,43 @@ module.exports = function (config) {
|
|
|
212
221
|
})
|
|
213
222
|
})
|
|
214
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
|
+
|
|
215
261
|
// Collect test results
|
|
216
262
|
event.dispatcher.on(event.test.finished, test => {
|
|
217
263
|
const testId = generateTestId(test)
|
|
@@ -373,8 +419,14 @@ module.exports = function (config) {
|
|
|
373
419
|
// Calculate stats from our collected test data instead of using result.stats
|
|
374
420
|
const passedTests = reportData.tests.filter(t => t.state === 'passed').length
|
|
375
421
|
const failedTests = reportData.tests.filter(t => t.state === 'failed').length
|
|
376
|
-
|
|
377
|
-
const
|
|
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)
|
|
378
430
|
|
|
379
431
|
// Populate failures from our collected test data with enhanced details
|
|
380
432
|
reportData.failures = reportData.tests
|
|
@@ -418,9 +470,10 @@ module.exports = function (config) {
|
|
|
418
470
|
passes: passedTests,
|
|
419
471
|
failures: failedTests,
|
|
420
472
|
pending: pendingTests,
|
|
421
|
-
skipped: skippedTests,
|
|
422
473
|
duration: reportData.duration,
|
|
423
474
|
failedHooks: result.stats?.failedHooks || 0,
|
|
475
|
+
flaky: flakyTests,
|
|
476
|
+
artifacts: totalArtifacts,
|
|
424
477
|
}
|
|
425
478
|
|
|
426
479
|
// Debug logging for final stats
|
|
@@ -803,6 +856,10 @@ module.exports = function (config) {
|
|
|
803
856
|
try {
|
|
804
857
|
const workerData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
|
|
805
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
|
+
|
|
806
863
|
// Merge stats
|
|
807
864
|
if (workerData.stats) {
|
|
808
865
|
consolidatedData.stats.passes += workerData.stats.passes || 0
|
|
@@ -814,8 +871,14 @@ module.exports = function (config) {
|
|
|
814
871
|
consolidatedData.stats.failedHooks += workerData.stats.failedHooks || 0
|
|
815
872
|
}
|
|
816
873
|
|
|
817
|
-
// Merge tests and
|
|
818
|
-
if (workerData.tests)
|
|
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
|
+
}
|
|
819
882
|
if (workerData.failures) consolidatedData.failures.push(...workerData.failures)
|
|
820
883
|
if (workerData.hooks) consolidatedData.hooks.push(...workerData.hooks)
|
|
821
884
|
if (workerData.retries) consolidatedData.retries.push(...workerData.retries)
|
|
@@ -932,6 +995,10 @@ module.exports = function (config) {
|
|
|
932
995
|
const failed = stats.failures || 0
|
|
933
996
|
const pending = stats.pending || 0
|
|
934
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'
|
|
935
1002
|
|
|
936
1003
|
return `
|
|
937
1004
|
<div class="stats-cards">
|
|
@@ -948,9 +1015,21 @@ module.exports = function (config) {
|
|
|
948
1015
|
<span class="stat-number">${failed}</span>
|
|
949
1016
|
</div>
|
|
950
1017
|
<div class="stat-card pending">
|
|
951
|
-
<h3>
|
|
1018
|
+
<h3>Skipped</h3>
|
|
952
1019
|
<span class="stat-number">${pending}</span>
|
|
953
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>
|
|
954
1033
|
</div>
|
|
955
1034
|
<div class="pie-chart-container">
|
|
956
1035
|
<canvas id="statsChart" width="300" height="300"></canvas>
|
|
@@ -971,49 +1050,73 @@ module.exports = function (config) {
|
|
|
971
1050
|
return '<p>No tests found.</p>'
|
|
972
1051
|
}
|
|
973
1052
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const featureDetails = test.isBdd && test.feature ? generateBddFeatureHtml(test.feature) : ''
|
|
982
|
-
const hooks = test.hooks && test.hooks.length > 0 ? generateHooksHtml(test.hooks) : ''
|
|
983
|
-
const artifacts = config.includeArtifacts && test.artifacts ? generateArtifactsHtml(test.artifacts, test.state === 'failed') : ''
|
|
984
|
-
const metadata = config.showMetadata && (test.meta || test.opts) ? generateMetadataHtml(test.meta, test.opts) : ''
|
|
985
|
-
const tags = config.showTags && test.tags && test.tags.length > 0 ? generateTagsHtml(test.tags) : ''
|
|
986
|
-
const retries = config.showRetries && test.retryAttempts > 0 ? generateTestRetryHtml(test.retryAttempts) : ''
|
|
987
|
-
const notes = test.notes && test.notes.length > 0 ? generateNotesHtml(test.notes) : ''
|
|
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
|
+
})
|
|
988
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()
|
|
989
1065
|
return `
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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('')}
|
|
1002
1117
|
</div>
|
|
1003
|
-
</
|
|
1004
|
-
|
|
1005
|
-
${test.err ? `<div class="error-message"><pre>${escapeHtml(getErrorMessage(test))}</pre></div>` : ''}
|
|
1006
|
-
${featureDetails}
|
|
1007
|
-
${tags}
|
|
1008
|
-
${metadata}
|
|
1009
|
-
${retries}
|
|
1010
|
-
${notes}
|
|
1011
|
-
${hooks}
|
|
1012
|
-
${steps}
|
|
1013
|
-
${artifacts}
|
|
1014
|
-
</div>
|
|
1015
|
-
</div>
|
|
1016
|
-
`
|
|
1118
|
+
</section>
|
|
1119
|
+
`
|
|
1017
1120
|
})
|
|
1018
1121
|
.join('')
|
|
1019
1122
|
}
|
|
@@ -1103,13 +1206,19 @@ module.exports = function (config) {
|
|
|
1103
1206
|
const statusClass = hook.status || 'unknown'
|
|
1104
1207
|
const hookType = hook.type || 'hook'
|
|
1105
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>` : ''
|
|
1106
1211
|
|
|
1107
1212
|
return `
|
|
1108
1213
|
<div class="hook-item ${statusClass}">
|
|
1109
1214
|
<span class="hook-status ${statusClass}">●</span>
|
|
1110
|
-
<
|
|
1111
|
-
|
|
1112
|
-
|
|
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>
|
|
1113
1222
|
</div>
|
|
1114
1223
|
`
|
|
1115
1224
|
})
|
|
@@ -1169,12 +1278,21 @@ module.exports = function (config) {
|
|
|
1169
1278
|
`
|
|
1170
1279
|
}
|
|
1171
1280
|
|
|
1172
|
-
function generateTestRetryHtml(retryAttempts) {
|
|
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
|
+
|
|
1173
1285
|
return `
|
|
1174
1286
|
<div class="retry-section">
|
|
1175
|
-
<h4>Retry
|
|
1287
|
+
<h4>Retry History:</h4>
|
|
1176
1288
|
<div class="retry-info">
|
|
1177
|
-
<
|
|
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>
|
|
1178
1296
|
</div>
|
|
1179
1297
|
</div>
|
|
1180
1298
|
`
|
|
@@ -1578,30 +1696,46 @@ module.exports = function (config) {
|
|
|
1578
1696
|
<!DOCTYPE html>
|
|
1579
1697
|
<html lang="en">
|
|
1580
1698
|
<head>
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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>
|
|
1585
1703
|
</head>
|
|
1586
1704
|
<body>
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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>
|
|
1594
1712
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1713
|
+
<main class="report-content">
|
|
1714
|
+
{{systemInfoHtml}}
|
|
1597
1715
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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>
|
|
1601
1733
|
</section>
|
|
1602
1734
|
|
|
1603
1735
|
<section class="history-section" style="display: {{showHistory}};">
|
|
1604
|
-
<h2>Test History</h2>
|
|
1736
|
+
<h2>Test Execution History</h2>
|
|
1737
|
+
<div class="history-stats" id="historyStats"></div>
|
|
1738
|
+
<div class="history-timeline" id="historyTimeline"></div>
|
|
1605
1739
|
<div class="history-chart-container">
|
|
1606
1740
|
<canvas id="historyChart" width="1600" height="600"></canvas>
|
|
1607
1741
|
</div>
|
|
@@ -1654,10 +1788,10 @@ module.exports = function (config) {
|
|
|
1654
1788
|
</div>
|
|
1655
1789
|
</section>
|
|
1656
1790
|
|
|
1657
|
-
<section class="retries-section" style="display:
|
|
1658
|
-
<h2>Test Retries</h2>
|
|
1791
|
+
<section class="retries-section" style="display: none;">
|
|
1792
|
+
<h2>Test Retries (Moved to Test Details)</h2>
|
|
1659
1793
|
<div class="retries-container">
|
|
1660
|
-
|
|
1794
|
+
<p>Retry information is now shown in each test's details section.</p>
|
|
1661
1795
|
</div>
|
|
1662
1796
|
</section>
|
|
1663
1797
|
|
|
@@ -1722,7 +1856,7 @@ body {
|
|
|
1722
1856
|
padding: 0 1rem;
|
|
1723
1857
|
}
|
|
1724
1858
|
|
|
1725
|
-
.stats-section, .tests-section, .retries-section, .filters-section, .history-section, .system-info-section {
|
|
1859
|
+
.stats-section, .tests-section, .retries-section, .filters-section, .history-section, .system-info-section, .test-performance-section {
|
|
1726
1860
|
background: white;
|
|
1727
1861
|
margin-bottom: 2rem;
|
|
1728
1862
|
border-radius: 8px;
|
|
@@ -1730,7 +1864,7 @@ body {
|
|
|
1730
1864
|
overflow: hidden;
|
|
1731
1865
|
}
|
|
1732
1866
|
|
|
1733
|
-
.stats-section h2, .tests-section h2, .retries-section h2, .filters-section h2, .history-section h2 {
|
|
1867
|
+
.stats-section h2, .tests-section h2, .retries-section h2, .filters-section h2, .history-section h2, .test-performance-section h2 {
|
|
1734
1868
|
background: #34495e;
|
|
1735
1869
|
color: white;
|
|
1736
1870
|
padding: 1rem;
|
|
@@ -1757,6 +1891,23 @@ body {
|
|
|
1757
1891
|
.stat-card.passed { background: #27ae60; }
|
|
1758
1892
|
.stat-card.failed { background: #e74c3c; }
|
|
1759
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
|
+
}
|
|
1760
1911
|
|
|
1761
1912
|
.stat-card h3 {
|
|
1762
1913
|
font-size: 0.9rem;
|
|
@@ -1784,6 +1935,54 @@ body {
|
|
|
1784
1935
|
height: auto;
|
|
1785
1936
|
}
|
|
1786
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
|
+
|
|
1787
1986
|
.test-item {
|
|
1788
1987
|
border-bottom: 1px solid #eee;
|
|
1789
1988
|
margin: 0;
|
|
@@ -1861,9 +2060,64 @@ body {
|
|
|
1861
2060
|
font-weight: bold;
|
|
1862
2061
|
}
|
|
1863
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
|
+
|
|
1864
2113
|
.test-duration {
|
|
1865
|
-
font-size: 0.
|
|
1866
|
-
|
|
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;
|
|
1867
2121
|
}
|
|
1868
2122
|
|
|
1869
2123
|
.test-details {
|
|
@@ -1901,27 +2155,39 @@ body {
|
|
|
1901
2155
|
|
|
1902
2156
|
.hook-item {
|
|
1903
2157
|
display: flex;
|
|
1904
|
-
align-items:
|
|
1905
|
-
padding: 0.
|
|
1906
|
-
border
|
|
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;
|
|
1907
2164
|
}
|
|
1908
2165
|
|
|
1909
2166
|
.hook-item:last-child {
|
|
1910
|
-
|
|
2167
|
+
margin-bottom: 0;
|
|
1911
2168
|
}
|
|
1912
2169
|
|
|
1913
2170
|
.hook-status {
|
|
1914
|
-
margin-right: 0.
|
|
2171
|
+
margin-right: 0.75rem;
|
|
2172
|
+
flex-shrink: 0;
|
|
2173
|
+
margin-top: 0.2rem;
|
|
1915
2174
|
}
|
|
1916
2175
|
|
|
1917
2176
|
.hook-status.passed { color: #27ae60; }
|
|
1918
2177
|
.hook-status.failed { color: #e74c3c; }
|
|
1919
2178
|
|
|
1920
|
-
.hook-
|
|
2179
|
+
.hook-content {
|
|
1921
2180
|
flex: 1;
|
|
2181
|
+
display: flex;
|
|
2182
|
+
flex-direction: column;
|
|
2183
|
+
gap: 0.25rem;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
.hook-title {
|
|
1922
2187
|
font-family: 'Courier New', monospace;
|
|
1923
2188
|
font-size: 0.9rem;
|
|
1924
2189
|
font-weight: bold;
|
|
2190
|
+
color: #2c3e50;
|
|
1925
2191
|
}
|
|
1926
2192
|
|
|
1927
2193
|
.hook-duration {
|
|
@@ -1929,8 +2195,13 @@ body {
|
|
|
1929
2195
|
color: #7f8c8d;
|
|
1930
2196
|
}
|
|
1931
2197
|
|
|
2198
|
+
.hook-location, .hook-context {
|
|
2199
|
+
font-size: 0.8rem;
|
|
2200
|
+
color: #6c757d;
|
|
2201
|
+
font-style: italic;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
1932
2204
|
.hook-error {
|
|
1933
|
-
width: 100%;
|
|
1934
2205
|
margin-top: 0.5rem;
|
|
1935
2206
|
padding: 0.5rem;
|
|
1936
2207
|
background: #fee;
|
|
@@ -2219,11 +2490,22 @@ body {
|
|
|
2219
2490
|
}
|
|
2220
2491
|
|
|
2221
2492
|
/* Retry Information */
|
|
2493
|
+
.retry-section {
|
|
2494
|
+
margin-top: 1rem;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2222
2497
|
.retry-info {
|
|
2223
|
-
padding:
|
|
2224
|
-
background: #
|
|
2498
|
+
padding: 1rem;
|
|
2499
|
+
background: #fff9e6;
|
|
2225
2500
|
border-radius: 4px;
|
|
2226
|
-
border-left:
|
|
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;
|
|
2227
2509
|
}
|
|
2228
2510
|
|
|
2229
2511
|
.retry-count {
|
|
@@ -2231,6 +2513,29 @@ body {
|
|
|
2231
2513
|
font-weight: 500;
|
|
2232
2514
|
}
|
|
2233
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
|
+
|
|
2234
2539
|
/* Retries Section */
|
|
2235
2540
|
.retry-item {
|
|
2236
2541
|
padding: 1rem;
|
|
@@ -2276,6 +2581,92 @@ body {
|
|
|
2276
2581
|
}
|
|
2277
2582
|
|
|
2278
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
|
+
|
|
2279
2670
|
.history-chart-container {
|
|
2280
2671
|
padding: 2rem 1rem;
|
|
2281
2672
|
display: flex;
|
|
@@ -2287,6 +2678,87 @@ body {
|
|
|
2287
2678
|
height: auto;
|
|
2288
2679
|
}
|
|
2289
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
|
+
|
|
2290
2762
|
/* Hidden items for filtering */
|
|
2291
2763
|
.test-item.filtered-out {
|
|
2292
2764
|
display: none !important;
|
|
@@ -2508,6 +2980,21 @@ body {
|
|
|
2508
2980
|
function scrollToTop() {
|
|
2509
2981
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
2510
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
|
+
|
|
2511
2998
|
function toggleTestDetails(testId) {
|
|
2512
2999
|
const details = document.getElementById('details-' + testId);
|
|
2513
3000
|
if (details.style.display === 'none' || details.style.display === '') {
|
|
@@ -2993,6 +3480,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
2993
3480
|
// Draw charts
|
|
2994
3481
|
drawPieChart();
|
|
2995
3482
|
drawHistoryChart();
|
|
3483
|
+
renderTestPerformance();
|
|
3484
|
+
renderHistoryTimeline();
|
|
3485
|
+
|
|
2996
3486
|
// Add Go to Top button
|
|
2997
3487
|
const goTopBtn = document.createElement('button');
|
|
2998
3488
|
goTopBtn.innerText = '↑ Top';
|
|
@@ -3018,6 +3508,141 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
3018
3508
|
document.getElementById('retryFilter').addEventListener('change', applyFilters);
|
|
3019
3509
|
document.getElementById('typeFilter').addEventListener('change', applyFilters);
|
|
3020
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
|
+
}
|
|
3021
3646
|
`
|
|
3022
3647
|
}
|
|
3023
3648
|
}
|