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.
Files changed (191) hide show
  1. package/README.md +1 -3
  2. package/bin/codecept.js +51 -53
  3. package/bin/test-server.js +14 -3
  4. package/docs/webapi/click.mustache +5 -1
  5. package/lib/actor.js +15 -11
  6. package/lib/ai.js +72 -107
  7. package/lib/assert/empty.js +9 -8
  8. package/lib/assert/equal.js +15 -17
  9. package/lib/assert/error.js +2 -2
  10. package/lib/assert/include.js +9 -11
  11. package/lib/assert/throws.js +1 -1
  12. package/lib/assert/truth.js +8 -5
  13. package/lib/assert.js +18 -18
  14. package/lib/codecept.js +102 -75
  15. package/lib/colorUtils.js +48 -50
  16. package/lib/command/check.js +32 -27
  17. package/lib/command/configMigrate.js +11 -10
  18. package/lib/command/definitions.js +16 -10
  19. package/lib/command/dryRun.js +16 -16
  20. package/lib/command/generate.js +62 -27
  21. package/lib/command/gherkin/init.js +36 -38
  22. package/lib/command/gherkin/snippets.js +14 -14
  23. package/lib/command/gherkin/steps.js +21 -18
  24. package/lib/command/info.js +8 -8
  25. package/lib/command/init.js +36 -29
  26. package/lib/command/interactive.js +11 -10
  27. package/lib/command/list.js +10 -9
  28. package/lib/command/run-multiple/chunk.js +5 -5
  29. package/lib/command/run-multiple/collection.js +5 -5
  30. package/lib/command/run-multiple/run.js +3 -3
  31. package/lib/command/run-multiple.js +16 -13
  32. package/lib/command/run-rerun.js +6 -7
  33. package/lib/command/run-workers.js +24 -9
  34. package/lib/command/run.js +23 -8
  35. package/lib/command/utils.js +20 -18
  36. package/lib/command/workers/runTests.js +197 -114
  37. package/lib/config.js +124 -51
  38. package/lib/container.js +438 -87
  39. package/lib/data/context.js +6 -5
  40. package/lib/data/dataScenarioConfig.js +1 -1
  41. package/lib/data/dataTableArgument.js +1 -1
  42. package/lib/data/table.js +1 -1
  43. package/lib/effects.js +94 -10
  44. package/lib/element/WebElement.js +2 -2
  45. package/lib/els.js +11 -9
  46. package/lib/event.js +11 -10
  47. package/lib/globals.js +141 -0
  48. package/lib/heal.js +12 -12
  49. package/lib/helper/AI.js +11 -11
  50. package/lib/helper/ApiDataFactory.js +50 -19
  51. package/lib/helper/Appium.js +19 -27
  52. package/lib/helper/FileSystem.js +32 -12
  53. package/lib/helper/GraphQL.js +3 -3
  54. package/lib/helper/GraphQLDataFactory.js +4 -4
  55. package/lib/helper/JSONResponse.js +25 -29
  56. package/lib/helper/Mochawesome.js +7 -4
  57. package/lib/helper/Playwright.js +902 -164
  58. package/lib/helper/Puppeteer.js +383 -76
  59. package/lib/helper/REST.js +29 -12
  60. package/lib/helper/WebDriver.js +268 -61
  61. package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
  62. package/lib/helper/errors/ConnectionRefused.js +6 -6
  63. package/lib/helper/errors/ElementAssertion.js +11 -16
  64. package/lib/helper/errors/ElementNotFound.js +5 -9
  65. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  66. package/lib/helper/extras/Console.js +11 -11
  67. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  68. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  69. package/lib/helper/extras/PlaywrightReactVueLocator.js +18 -9
  70. package/lib/helper/extras/PlaywrightRestartOpts.js +34 -23
  71. package/lib/helper/extras/Popup.js +1 -1
  72. package/lib/helper/extras/React.js +29 -30
  73. package/lib/helper/network/actions.js +29 -44
  74. package/lib/helper/network/utils.js +76 -83
  75. package/lib/helper/scripts/blurElement.js +6 -6
  76. package/lib/helper/scripts/focusElement.js +6 -6
  77. package/lib/helper/scripts/highlightElement.js +9 -9
  78. package/lib/helper/scripts/isElementClickable.js +34 -34
  79. package/lib/helper.js +2 -1
  80. package/lib/history.js +23 -20
  81. package/lib/hooks.js +10 -10
  82. package/lib/html.js +90 -100
  83. package/lib/index.js +48 -21
  84. package/lib/listener/config.js +19 -12
  85. package/lib/listener/emptyRun.js +6 -7
  86. package/lib/listener/enhancedGlobalRetry.js +6 -6
  87. package/lib/listener/exit.js +4 -3
  88. package/lib/listener/globalRetry.js +5 -5
  89. package/lib/listener/globalTimeout.js +30 -14
  90. package/lib/listener/helpers.js +39 -14
  91. package/lib/listener/mocha.js +3 -4
  92. package/lib/listener/result.js +4 -5
  93. package/lib/listener/retryEnhancer.js +3 -3
  94. package/lib/listener/steps.js +8 -7
  95. package/lib/listener/store.js +3 -3
  96. package/lib/locator.js +213 -192
  97. package/lib/mocha/asyncWrapper.js +105 -62
  98. package/lib/mocha/bdd.js +99 -13
  99. package/lib/mocha/cli.js +59 -26
  100. package/lib/mocha/factory.js +78 -19
  101. package/lib/mocha/featureConfig.js +1 -1
  102. package/lib/mocha/gherkin.js +56 -24
  103. package/lib/mocha/hooks.js +12 -3
  104. package/lib/mocha/index.js +13 -4
  105. package/lib/mocha/inject.js +22 -5
  106. package/lib/mocha/scenarioConfig.js +2 -2
  107. package/lib/mocha/suite.js +9 -2
  108. package/lib/mocha/test.js +10 -7
  109. package/lib/mocha/ui.js +28 -18
  110. package/lib/output.js +10 -8
  111. package/lib/parser.js +44 -44
  112. package/lib/pause.js +15 -16
  113. package/lib/plugin/analyze.js +19 -12
  114. package/lib/plugin/auth.js +20 -21
  115. package/lib/plugin/autoDelay.js +12 -8
  116. package/lib/plugin/coverage.js +28 -11
  117. package/lib/plugin/customLocator.js +3 -3
  118. package/lib/plugin/customReporter.js +3 -2
  119. package/lib/plugin/enhancedRetryFailedStep.js +6 -6
  120. package/lib/plugin/heal.js +14 -9
  121. package/lib/plugin/htmlReporter.js +724 -99
  122. package/lib/plugin/pageInfo.js +10 -10
  123. package/lib/plugin/pauseOnFail.js +4 -3
  124. package/lib/plugin/retryFailedStep.js +48 -5
  125. package/lib/plugin/screenshotOnFail.js +75 -37
  126. package/lib/plugin/stepByStepReport.js +14 -14
  127. package/lib/plugin/stepTimeout.js +4 -3
  128. package/lib/plugin/subtitles.js +6 -5
  129. package/lib/recorder.js +33 -14
  130. package/lib/rerun.js +69 -26
  131. package/lib/result.js +4 -4
  132. package/lib/retryCoordinator.js +2 -2
  133. package/lib/secret.js +18 -17
  134. package/lib/session.js +95 -89
  135. package/lib/step/base.js +7 -7
  136. package/lib/step/comment.js +2 -2
  137. package/lib/step/config.js +1 -1
  138. package/lib/step/func.js +3 -3
  139. package/lib/step/helper.js +3 -3
  140. package/lib/step/meta.js +5 -5
  141. package/lib/step/record.js +11 -11
  142. package/lib/step/retry.js +3 -3
  143. package/lib/step/section.js +3 -3
  144. package/lib/step.js +7 -10
  145. package/lib/steps.js +9 -5
  146. package/lib/store.js +1 -1
  147. package/lib/template/heal.js +1 -1
  148. package/lib/template/prompts/generatePageObject.js +31 -0
  149. package/lib/template/prompts/healStep.js +13 -0
  150. package/lib/template/prompts/writeStep.js +9 -0
  151. package/lib/test-server.js +17 -6
  152. package/lib/timeout.js +1 -7
  153. package/lib/transform.js +8 -8
  154. package/lib/translation.js +32 -18
  155. package/lib/utils/mask_data.js +4 -10
  156. package/lib/utils.js +66 -64
  157. package/lib/workerStorage.js +17 -17
  158. package/lib/workers.js +214 -84
  159. package/package.json +41 -37
  160. package/translations/de-DE.js +2 -2
  161. package/translations/fr-FR.js +2 -2
  162. package/translations/index.js +23 -10
  163. package/translations/it-IT.js +2 -2
  164. package/translations/ja-JP.js +2 -2
  165. package/translations/nl-NL.js +2 -2
  166. package/translations/pl-PL.js +2 -2
  167. package/translations/pt-BR.js +2 -2
  168. package/translations/ru-RU.js +2 -2
  169. package/translations/utils.js +4 -3
  170. package/translations/zh-CN.js +2 -2
  171. package/translations/zh-TW.js +2 -2
  172. package/typings/index.d.ts +5 -3
  173. package/typings/promiseBasedTypes.d.ts +4 -0
  174. package/typings/types.d.ts +4 -0
  175. package/lib/helper/Nightmare.js +0 -1486
  176. package/lib/helper/Protractor.js +0 -1840
  177. package/lib/helper/TestCafe.js +0 -1391
  178. package/lib/helper/clientscripts/nightmare.js +0 -213
  179. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  180. package/lib/helper/testcafe/testcafe-utils.js +0 -61
  181. package/lib/plugin/allure.js +0 -15
  182. package/lib/plugin/autoLogin.js +0 -5
  183. package/lib/plugin/commentStep.js +0 -141
  184. package/lib/plugin/eachElement.js +0 -127
  185. package/lib/plugin/fakerTransform.js +0 -49
  186. package/lib/plugin/retryTo.js +0 -16
  187. package/lib/plugin/selenoid.js +0 -364
  188. package/lib/plugin/standardActingHelpers.js +0 -6
  189. package/lib/plugin/tryTo.js +0 -16
  190. package/lib/plugin/wdio.js +0 -247
  191. 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
- const fs = require('fs')
6
- const path = require('path')
7
- const mkdirp = require('mkdirp')
8
- const crypto = require('crypto')
9
- const { threadId } = require('worker_threads')
10
- const { template } = require('../utils')
11
- const { getMachineInfo } = require('../command/info')
12
-
13
- const event = require('../event')
14
- const output = require('../output')
15
- const Codecept = require('../codecept')
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
- module.exports = function (config) {
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
- const pendingTests = reportData.tests.filter(t => t.state === 'pending').length
377
- const skippedTests = reportData.tests.filter(t => t.state === 'skipped').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)
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 failures
818
- if (workerData.tests) consolidatedData.tests.push(...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>Pending</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
- return tests
975
- .map(test => {
976
- const statusClass = test.state || 'unknown'
977
- // Use preserved parent/suite titles (for worker mode) or fallback to direct access
978
- const feature = test.isBdd && test.feature ? test.feature.name : test.parentTitle || test.suiteTitle || test.parent?.title || test.suite?.title || 'Unknown Feature'
979
- // Always try to show steps if available, even for unknown feature
980
- const steps = config.showSteps && test.steps ? (test.isBdd ? generateBddStepsHtml(test.steps) : generateStepsHtml(test.steps)) : ''
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
- <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'}">
991
- <div class="test-header" onclick="toggleTestDetails('test-${test.id}')">
992
- <span class="test-status ${statusClass}">●</span>
993
- <div class="test-info">
994
- <h3 class="test-title">${test.isBdd ? `Scenario: ${test.title}` : test.title}</h3>
995
- <div class="test-meta-line">
996
- <span class="test-feature">${test.isBdd ? 'Feature: ' : ''}${feature}</span>
997
- ${test.uid ? `<span class="test-uid">${test.uid}</span>` : ''}
998
- <span class="test-duration">${formatDuration(test.duration)}</span>
999
- ${test.retryAttempts > 0 ? `<span class="retry-badge">${test.retryAttempts} retries</span>` : ''}
1000
- ${test.isBdd ? '<span class="bdd-badge">Gherkin</span>' : ''}
1001
- </div>
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
- </div>
1004
- <div class="test-details" id="details-test-${test.id}">
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
- <span class="hook-title">${hookType}: ${hookTitle}</span>
1111
- <span class="hook-duration">${formatDuration(hook.duration)}</span>
1112
- ${hook.error ? `<div class="hook-error">${escapeHtml(hook.error)}</div>` : ''}
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 Information:</h4>
1287
+ <h4>Retry History:</h4>
1176
1288
  <div class="retry-info">
1177
- <span class="retry-count">Total retry attempts: <strong>${retryAttempts}</strong></span>
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
- <meta charset="UTF-8">
1582
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1583
- <title>{{title}}</title>
1584
- <style>{{cssStyles}}</style>
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
- <header class="report-header">
1588
- <h1>{{title}}</h1>
1589
- <div class="report-meta">
1590
- <span>Generated: {{timestamp}}</span>
1591
- <span>Duration: {{duration}}</span>
1592
- </div>
1593
- </header>
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
- <main class="report-content">
1596
- {{systemInfoHtml}}
1713
+ <main class="report-content">
1714
+ {{systemInfoHtml}}
1597
1715
 
1598
- <section class="stats-section">
1599
- <h2>Test Statistics</h2>
1600
- {{statsHtml}}
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: {{showRetries}};">
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
- {{retriesHtml}}
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.8rem;
1866
- color: #7f8c8d;
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: center;
1905
- padding: 0.5rem 0;
1906
- border-bottom: 1px solid #ecf0f1;
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
- border-bottom: none;
2167
+ margin-bottom: 0;
1911
2168
  }
1912
2169
 
1913
2170
  .hook-status {
1914
- margin-right: 0.5rem;
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-title {
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: 0.5rem;
2224
- background: #fef9e7;
2498
+ padding: 1rem;
2499
+ background: #fff9e6;
2225
2500
  border-radius: 4px;
2226
- border-left: 3px solid #f39c12;
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
  }