donobu 5.22.0 → 5.24.0

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 (117) hide show
  1. package/dist/apis/SuitesApi.d.ts +36 -0
  2. package/dist/apis/SuitesApi.js +68 -0
  3. package/dist/apis/TestsApi.d.ts +40 -0
  4. package/dist/apis/TestsApi.js +86 -0
  5. package/dist/assets/openapi-schema.yaml +15 -0
  6. package/dist/cli/playwright-json-to-html.js +113 -52
  7. package/dist/esm/apis/SuitesApi.d.ts +36 -0
  8. package/dist/esm/apis/SuitesApi.js +68 -0
  9. package/dist/esm/apis/TestsApi.d.ts +40 -0
  10. package/dist/esm/apis/TestsApi.js +86 -0
  11. package/dist/esm/assets/openapi-schema.yaml +15 -0
  12. package/dist/esm/cli/playwright-json-to-html.js +113 -52
  13. package/dist/esm/lib/ai/cache/assertCache.d.ts +32 -1
  14. package/dist/esm/lib/ai/cache/assertCache.js +9 -0
  15. package/dist/esm/lib/ai/cache/cache.d.ts +13 -2
  16. package/dist/esm/lib/ai/cache/cache.js +123 -7
  17. package/dist/esm/lib/ai/locate/LocateException.d.ts +16 -0
  18. package/dist/esm/lib/ai/locate/LocateException.js +21 -0
  19. package/dist/esm/lib/ai/locate/buildLocator.d.ts +9 -0
  20. package/dist/esm/lib/ai/locate/buildLocator.js +75 -0
  21. package/dist/esm/lib/ai/locate/domSnapshot.d.ts +21 -0
  22. package/dist/esm/lib/ai/locate/domSnapshot.js +459 -0
  23. package/dist/esm/lib/ai/locate/locateElement.d.ts +24 -0
  24. package/dist/esm/lib/ai/locate/locateElement.js +226 -0
  25. package/dist/esm/lib/ai/locate/locateSchema.d.ts +33 -0
  26. package/dist/esm/lib/ai/locate/locateSchema.js +88 -0
  27. package/dist/esm/lib/ai/locate/locateTypes.d.ts +57 -0
  28. package/dist/esm/lib/ai/locate/locateTypes.js +3 -0
  29. package/dist/esm/lib/page/DonobuExtendedPage.d.ts +60 -14
  30. package/dist/esm/lib/page/extendPage.js +49 -0
  31. package/dist/esm/lib/test/utils/triageTestFailure.js +35 -10
  32. package/dist/esm/main.d.ts +1 -0
  33. package/dist/esm/managers/AdminApiController.d.ts +8 -0
  34. package/dist/esm/managers/AdminApiController.js +29 -0
  35. package/dist/esm/managers/DonobuFlowsManager.d.ts +29 -1
  36. package/dist/esm/managers/DonobuFlowsManager.js +74 -28
  37. package/dist/esm/managers/DonobuStack.js +1 -1
  38. package/dist/esm/managers/FederatedPagination.js +10 -1
  39. package/dist/esm/managers/FlowCatalog.js +31 -39
  40. package/dist/esm/managers/FlowDependencyAnalyzer.js +12 -0
  41. package/dist/esm/managers/SuitesManager.js +23 -6
  42. package/dist/esm/managers/TestsManager.d.ts +24 -3
  43. package/dist/esm/managers/TestsManager.js +123 -28
  44. package/dist/esm/models/BrowserConfig.d.ts +3 -0
  45. package/dist/esm/models/BrowserStateFlowReference.d.ts +3 -0
  46. package/dist/esm/models/BrowserStateFlowReference.js +13 -2
  47. package/dist/esm/models/CreateDonobuFlow.d.ts +4 -0
  48. package/dist/esm/models/CreateDonobuFlow.js +4 -0
  49. package/dist/esm/models/CreateSuite.d.ts +3 -0
  50. package/dist/esm/models/CreateTest.d.ts +3 -0
  51. package/dist/esm/models/FlowMetadata.d.ts +4 -0
  52. package/dist/esm/models/FlowMetadata.js +8 -0
  53. package/dist/esm/models/RunConfig.d.ts +6 -0
  54. package/dist/esm/models/SuiteMetadata.d.ts +3 -0
  55. package/dist/esm/models/TargetConfig.d.ts +3 -0
  56. package/dist/esm/models/TestMetadata.d.ts +3 -0
  57. package/dist/esm/persistence/flows/FlowsPersistenceDonobuApi.js +7 -0
  58. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js +11 -0
  59. package/dist/esm/persistence/flows/FlowsPersistenceVolatile.js +7 -1
  60. package/dist/esm/tools/AssertPageTool.d.ts +2 -2
  61. package/dist/esm/tools/AssertTool.js +1 -4
  62. package/dist/esm/tools/TriggerDonobuFlowTool.d.ts +8 -0
  63. package/dist/esm/utils/MiscUtils.d.ts +13 -0
  64. package/dist/esm/utils/MiscUtils.js +21 -0
  65. package/dist/lib/ai/cache/assertCache.d.ts +32 -1
  66. package/dist/lib/ai/cache/assertCache.js +9 -0
  67. package/dist/lib/ai/cache/cache.d.ts +13 -2
  68. package/dist/lib/ai/cache/cache.js +123 -7
  69. package/dist/lib/ai/locate/LocateException.d.ts +16 -0
  70. package/dist/lib/ai/locate/LocateException.js +21 -0
  71. package/dist/lib/ai/locate/buildLocator.d.ts +9 -0
  72. package/dist/lib/ai/locate/buildLocator.js +75 -0
  73. package/dist/lib/ai/locate/domSnapshot.d.ts +21 -0
  74. package/dist/lib/ai/locate/domSnapshot.js +459 -0
  75. package/dist/lib/ai/locate/locateElement.d.ts +24 -0
  76. package/dist/lib/ai/locate/locateElement.js +226 -0
  77. package/dist/lib/ai/locate/locateSchema.d.ts +33 -0
  78. package/dist/lib/ai/locate/locateSchema.js +88 -0
  79. package/dist/lib/ai/locate/locateTypes.d.ts +57 -0
  80. package/dist/lib/ai/locate/locateTypes.js +3 -0
  81. package/dist/lib/page/DonobuExtendedPage.d.ts +60 -14
  82. package/dist/lib/page/extendPage.js +49 -0
  83. package/dist/lib/test/utils/triageTestFailure.js +35 -10
  84. package/dist/main.d.ts +1 -0
  85. package/dist/managers/AdminApiController.d.ts +8 -0
  86. package/dist/managers/AdminApiController.js +29 -0
  87. package/dist/managers/DonobuFlowsManager.d.ts +29 -1
  88. package/dist/managers/DonobuFlowsManager.js +74 -28
  89. package/dist/managers/DonobuStack.js +1 -1
  90. package/dist/managers/FederatedPagination.js +10 -1
  91. package/dist/managers/FlowCatalog.js +31 -39
  92. package/dist/managers/FlowDependencyAnalyzer.js +12 -0
  93. package/dist/managers/SuitesManager.js +23 -6
  94. package/dist/managers/TestsManager.d.ts +24 -3
  95. package/dist/managers/TestsManager.js +123 -28
  96. package/dist/models/BrowserConfig.d.ts +3 -0
  97. package/dist/models/BrowserStateFlowReference.d.ts +3 -0
  98. package/dist/models/BrowserStateFlowReference.js +13 -2
  99. package/dist/models/CreateDonobuFlow.d.ts +4 -0
  100. package/dist/models/CreateDonobuFlow.js +4 -0
  101. package/dist/models/CreateSuite.d.ts +3 -0
  102. package/dist/models/CreateTest.d.ts +3 -0
  103. package/dist/models/FlowMetadata.d.ts +4 -0
  104. package/dist/models/FlowMetadata.js +8 -0
  105. package/dist/models/RunConfig.d.ts +6 -0
  106. package/dist/models/SuiteMetadata.d.ts +3 -0
  107. package/dist/models/TargetConfig.d.ts +3 -0
  108. package/dist/models/TestMetadata.d.ts +3 -0
  109. package/dist/persistence/flows/FlowsPersistenceDonobuApi.js +7 -0
  110. package/dist/persistence/flows/FlowsPersistenceSqlite.js +11 -0
  111. package/dist/persistence/flows/FlowsPersistenceVolatile.js +7 -1
  112. package/dist/tools/AssertPageTool.d.ts +2 -2
  113. package/dist/tools/AssertTool.js +1 -4
  114. package/dist/tools/TriggerDonobuFlowTool.d.ts +8 -0
  115. package/dist/utils/MiscUtils.d.ts +13 -0
  116. package/dist/utils/MiscUtils.js +21 -0
  117. package/package.json +1 -1
@@ -0,0 +1,36 @@
1
+ import type { Request, Response } from 'express';
2
+ import type { SuitesManager } from '../managers/SuitesManager';
3
+ import type { TestsManager } from '../managers/TestsManager';
4
+ /**
5
+ * API controller for managing suites.
6
+ */
7
+ export declare class SuitesApi {
8
+ private readonly suitesManager;
9
+ private readonly testsManager;
10
+ constructor(suitesManager: SuitesManager, testsManager: TestsManager);
11
+ /**
12
+ * GET /api/suites — list suites with optional filtering and pagination.
13
+ */
14
+ getSuites(req: Request, res: Response): Promise<void>;
15
+ /**
16
+ * POST /api/suites — create a new suite.
17
+ */
18
+ createSuite(req: Request, res: Response): Promise<void>;
19
+ /**
20
+ * GET /api/suites/:suiteId — get a suite by ID.
21
+ */
22
+ getSuite(req: Request, res: Response): Promise<void>;
23
+ /**
24
+ * PUT /api/suites/:suiteId — update an existing suite.
25
+ */
26
+ updateSuite(req: Request, res: Response): Promise<void>;
27
+ /**
28
+ * DELETE /api/suites/:suiteId — delete a suite (tests are orphaned).
29
+ */
30
+ deleteSuite(req: Request, res: Response): Promise<void>;
31
+ /**
32
+ * GET /api/suites/:suiteId/tests — list tests in a suite.
33
+ */
34
+ getSuiteTests(req: Request, res: Response): Promise<void>;
35
+ }
36
+ //# sourceMappingURL=SuitesApi.d.ts.map
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SuitesApi = void 0;
4
+ const CreateSuite_1 = require("../models/CreateSuite");
5
+ const SuiteMetadata_1 = require("../models/SuiteMetadata");
6
+ /**
7
+ * API controller for managing suites.
8
+ */
9
+ class SuitesApi {
10
+ constructor(suitesManager, testsManager) {
11
+ this.suitesManager = suitesManager;
12
+ this.testsManager = testsManager;
13
+ }
14
+ /**
15
+ * GET /api/suites — list suites with optional filtering and pagination.
16
+ */
17
+ async getSuites(req, res) {
18
+ const query = SuiteMetadata_1.SuitesQuerySchema.parse(req.query);
19
+ const result = await this.suitesManager.getSuites(query);
20
+ res.json({ suites: result.items, nextPageToken: result.nextPageToken });
21
+ }
22
+ /**
23
+ * POST /api/suites — create a new suite.
24
+ */
25
+ async createSuite(req, res) {
26
+ const params = CreateSuite_1.CreateSuiteSchema.parse(req.body);
27
+ const suite = await this.suitesManager.createSuite(params);
28
+ res.json(suite);
29
+ }
30
+ /**
31
+ * GET /api/suites/:suiteId — get a suite by ID.
32
+ */
33
+ async getSuite(req, res) {
34
+ const suiteId = String(req.params.suiteId);
35
+ const suite = await this.suitesManager.getSuiteById(suiteId);
36
+ res.json(suite);
37
+ }
38
+ /**
39
+ * PUT /api/suites/:suiteId — update an existing suite.
40
+ */
41
+ async updateSuite(req, res) {
42
+ const suiteId = String(req.params.suiteId);
43
+ const suiteMetadata = SuiteMetadata_1.SuiteMetadataSchema.parse({
44
+ ...req.body,
45
+ id: suiteId,
46
+ });
47
+ await this.suitesManager.updateSuite(suiteMetadata);
48
+ res.json(suiteMetadata);
49
+ }
50
+ /**
51
+ * DELETE /api/suites/:suiteId — delete a suite (tests are orphaned).
52
+ */
53
+ async deleteSuite(req, res) {
54
+ const suiteId = String(req.params.suiteId);
55
+ await this.suitesManager.deleteSuite(suiteId);
56
+ res.status(200).json({ deleted: true });
57
+ }
58
+ /**
59
+ * GET /api/suites/:suiteId/tests — list tests in a suite.
60
+ */
61
+ async getSuiteTests(req, res) {
62
+ const suiteId = String(req.params.suiteId);
63
+ const result = await this.testsManager.getTests({ suiteId });
64
+ res.json({ tests: result.items, nextPageToken: result.nextPageToken });
65
+ }
66
+ }
67
+ exports.SuitesApi = SuitesApi;
68
+ //# sourceMappingURL=SuitesApi.js.map
@@ -0,0 +1,40 @@
1
+ import type { Request, Response } from 'express';
2
+ import type { DonobuFlowsManager } from '../managers/DonobuFlowsManager';
3
+ import type { TestsManager } from '../managers/TestsManager';
4
+ /**
5
+ * API controller for managing tests.
6
+ */
7
+ export declare class TestsApi {
8
+ private readonly testsManager;
9
+ private readonly flowsManager;
10
+ constructor(testsManager: TestsManager, flowsManager: DonobuFlowsManager);
11
+ /**
12
+ * GET /api/tests — list tests with optional filtering and pagination.
13
+ */
14
+ getTests(req: Request, res: Response): Promise<void>;
15
+ /**
16
+ * POST /api/tests — create a new test.
17
+ */
18
+ createTest(req: Request, res: Response): Promise<void>;
19
+ /**
20
+ * GET /api/tests/:testId — get a test by ID.
21
+ */
22
+ getTest(req: Request, res: Response): Promise<void>;
23
+ /**
24
+ * PUT /api/tests/:testId — update an existing test.
25
+ */
26
+ updateTest(req: Request, res: Response): Promise<void>;
27
+ /**
28
+ * DELETE /api/tests/:testId — delete a test and all its flows.
29
+ */
30
+ deleteTest(req: Request, res: Response): Promise<void>;
31
+ /**
32
+ * GET /api/tests/:testId/flows — list flows for a test.
33
+ */
34
+ getTestFlows(req: Request, res: Response): Promise<void>;
35
+ /**
36
+ * POST /api/tests/:testId/run — execute a test (create a new flow from it).
37
+ */
38
+ runTest(req: Request, res: Response): Promise<void>;
39
+ }
40
+ //# sourceMappingURL=TestsApi.d.ts.map
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TestsApi = void 0;
4
+ const CreateTest_1 = require("../models/CreateTest");
5
+ const TestMetadata_1 = require("../models/TestMetadata");
6
+ /**
7
+ * API controller for managing tests.
8
+ */
9
+ class TestsApi {
10
+ constructor(testsManager, flowsManager) {
11
+ this.testsManager = testsManager;
12
+ this.flowsManager = flowsManager;
13
+ }
14
+ /**
15
+ * GET /api/tests — list tests with optional filtering and pagination.
16
+ */
17
+ async getTests(req, res) {
18
+ const query = TestMetadata_1.TestsQuerySchema.parse(req.query);
19
+ const result = await this.testsManager.getTests(query);
20
+ res.json({ tests: result.items, nextPageToken: result.nextPageToken });
21
+ }
22
+ /**
23
+ * POST /api/tests — create a new test.
24
+ */
25
+ async createTest(req, res) {
26
+ const params = CreateTest_1.CreateTestSchema.parse(req.body);
27
+ const test = await this.testsManager.createTest(params);
28
+ res.json(test);
29
+ }
30
+ /**
31
+ * GET /api/tests/:testId — get a test by ID.
32
+ */
33
+ async getTest(req, res) {
34
+ const testId = String(req.params.testId);
35
+ const test = await this.testsManager.getTestById(testId);
36
+ res.json(test);
37
+ }
38
+ /**
39
+ * PUT /api/tests/:testId — update an existing test.
40
+ */
41
+ async updateTest(req, res) {
42
+ const testId = String(req.params.testId);
43
+ const testMetadata = TestMetadata_1.TestMetadataSchema.parse({
44
+ ...req.body,
45
+ id: testId,
46
+ });
47
+ await this.testsManager.updateTest(testMetadata);
48
+ res.json(testMetadata);
49
+ }
50
+ /**
51
+ * DELETE /api/tests/:testId — delete a test and all its flows.
52
+ */
53
+ async deleteTest(req, res) {
54
+ const testId = String(req.params.testId);
55
+ await this.testsManager.deleteTest(testId);
56
+ res.status(200).json({ deleted: true });
57
+ }
58
+ /**
59
+ * GET /api/tests/:testId/flows — list flows for a test.
60
+ */
61
+ async getTestFlows(req, res) {
62
+ const testId = String(req.params.testId);
63
+ const flows = await this.flowsManager.getFlows({ testId });
64
+ res.json({ flows: flows.items, nextPageToken: flows.nextPageToken });
65
+ }
66
+ /**
67
+ * POST /api/tests/:testId/run — execute a test (create a new flow from it).
68
+ */
69
+ async runTest(req, res) {
70
+ const testId = String(req.params.testId);
71
+ const newFlowConfig = await this.testsManager.getNewFlowFromTest(testId);
72
+ const flowHandle = await this.flowsManager.createFlow(newFlowConfig);
73
+ // After starting an autonomous run, switch the test to deterministic
74
+ // so subsequent runs replay the recorded actions.
75
+ if (newFlowConfig.initialRunMode === 'AUTONOMOUS') {
76
+ const test = await this.testsManager.getTestById(testId);
77
+ await this.testsManager.updateTest({
78
+ ...test,
79
+ nextRunMode: 'DETERMINISTIC',
80
+ });
81
+ }
82
+ res.json(flowHandle.donobuFlow.metadata);
83
+ }
84
+ }
85
+ exports.TestsApi = TestsApi;
86
+ //# sourceMappingURL=TestsApi.js.map
@@ -1496,6 +1496,7 @@ components:
1496
1496
  to restore when starting a flow. Exactly one variant should be provided:
1497
1497
  - `id` — Reference a flow by its ID and restore its persisted browser state.
1498
1498
  - `name` — Reference a flow by its name and restore its persisted browser state.
1499
+ - `testId` — Reference a test by its ID and restore browser state from the most recent successful flow belonging to that test.
1499
1500
  - `json` — Provide the browser state inline as a JSON object.
1500
1501
  oneOf:
1501
1502
  - type: 'object'
@@ -1526,6 +1527,20 @@ components:
1526
1527
  value:
1527
1528
  type: 'string'
1528
1529
  description: 'The name of the flow whose browser state should be restored.'
1530
+ - type: 'object'
1531
+ description: 'Reference a test by ID to restore browser state from its most recent successful flow.'
1532
+ required:
1533
+ - 'type'
1534
+ - 'value'
1535
+ properties:
1536
+ type:
1537
+ type: 'string'
1538
+ enum:
1539
+ - 'testId'
1540
+ description: 'Indicates the value is a test ID.'
1541
+ value:
1542
+ type: 'string'
1543
+ description: 'A test ID. The browser state will be restored from the most recent successful flow belonging to this test.'
1529
1544
  - type: 'object'
1530
1545
  description: 'Provide browser state inline as a JSON object.'
1531
1546
  required:
@@ -425,8 +425,8 @@ function extractTests(jsonData) {
425
425
  const STATUS_CFG = {
426
426
  passed: {
427
427
  label: 'Passed',
428
- color: '#FF7F3A',
429
- bg: 'rgba(255,127,58,0.08)',
428
+ color: '#10b981',
429
+ bg: 'rgba(16,185,129,0.08)',
430
430
  icon: '&#10003;',
431
431
  },
432
432
  failed: {
@@ -1222,7 +1222,7 @@ function renderResultTimeline(results, outputDir) {
1222
1222
  html += '</div>';
1223
1223
  return html;
1224
1224
  }
1225
- const LOGO_SVG = '<svg viewBox="0 0 245 238" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#logo-clip)"><path d="M8.75 123.921L21.3889 110.367H32.0833L40.8333 133.602L43.75 155.869L32.0833 158.774L19.4444 155.869L8.75 123.921Z" fill="#FF7F3A"/><path d="M237.225 125.857L224.586 112.303H213.891L205.141 135.539L202.225 157.806L213.891 160.71L226.53 157.806L237.225 125.857Z" fill="#FF7F3A"/><path d="M130.276 57.1198L55.415 78.4187L46.665 88.1V142.315V164.583L68.0539 206.212L93.3317 225.575L117.637 231.384C130.276 230.416 156.332 227.898 159.443 225.575C163.332 222.671 186.665 192.658 190.554 182.977C194.443 173.296 195.415 157.806 195.415 147.156C195.415 136.507 196.387 120.048 195.415 115.208C194.637 111.335 177.591 89.0682 169.165 78.4187L130.276 57.1198Z" fill="#FF7F3A"/><path d="M129.868 30.4824C131.882 28.9302 134.006 26.8043 135.359 24.6522C136.96 22.1107 137.166 19.9586 138.26 17.5486C138.487 17.0434 138.418 16.0226 139.353 16.8172C139.565 16.9961 141.99 20.9373 142.291 21.5214C144.854 26.5781 145.435 32.282 145.943 37.8597C168.385 44.6265 188.639 58.1076 200.824 78.3555C205.004 85.296 209.099 93.9466 210.108 101.971C224.772 94.6044 239.515 103.244 243.626 118.262C250.342 142.793 231.583 169.955 205.421 169.855C204.697 174.096 204.153 178.232 202.975 182.4C196.089 206.684 169.943 227.994 146.101 234.582C99.7856 247.374 45.9189 218.933 39.5938 169.782C13.2839 169.982-5.80773 142.093 1.61651 117.51C5.93895 103.192 20.5496 94.6201 34.5791 101.955C35.3453 101.903 36.3176 96.3197 36.6875 95.1358C41.9188 78.566 53.1846 63.2537 67.0819 52.8614C80.1232 43.1111 99.2202 37.8281 107.247 22.9421C111.099 15.7964 111.564 7.89294 110.333-.0157471C123.083 4.47268 130.724 17.1855 129.863 30.4719L129.868 30.4824ZM121.953 106.181C116.272 96.8144 103.95 89.0267 93.0272 87.4008C79.1774 85.3434 65.745 91.2788 58.8492 103.544C48.8727 121.288 56.6246 142.414 74.9606 150.349C50.8279 179.885 74.8179 211.972 107.157 218.949C130.492 223.985 160.295 216.781 173.69 195.828C183.032 181.216 180.908 163.446 169.98 150.349C195.931 140.352 197.643 103.692 172.971 90.8526C155.042 81.5232 133.636 89.8213 123.041 105.849L121.953 106.181ZM36.9517 154.538C35.9107 151.496 35.1075 148.334 34.4787 145.182C33.4219 139.889 32.328 133.38 32.1801 128.018C32.0057 121.793 35.266 115.552 26.954 113.774C15.5138 111.327 12.3803 123.834 13.2628 132.664C14.4306 144.345 24.3067 156.327 36.957 154.538H36.9517ZM217.876 113.658C216.338 113.968 212.423 116.331 212.195 118.036C212.053 119.12 212.734 120.425 212.761 121.666C212.882 127.481 212.407 133.658 211.53 139.41C210.8 144.193 209.553 149.255 208.063 153.864C207.921 155.969 213.178 154.69 214.436 154.401C222.732 152.512 229.21 144.277 231.055 136.253C233.237 126.734 231.261 110.937 217.871 113.663L217.876 113.658Z" fill="black"/><path d="M158.212 170.413C162.36 169.755 164.738 174.88 161.018 178.48C148.087 190.971 111.162 196.554 95.0185 189.509C91.848 188.125 91.1293 183.589 94.2998 181.958C96.5192 180.816 102.902 183.021 105.904 183.399C119.711 185.136 134.507 183.226 147.104 177.248C149.308 176.206 157.44 170.539 158.206 170.413H158.212Z" fill="black"/><path d="M150.305 114.72C163.975 111.91 165.682 135.542 153.835 137.152C140.884 138.915 139.943 116.846 150.305 114.72Z" fill="black"/><path d="M92.7645 114.721C103.808 112.437 108.141 129.933 99.2058 135.853C88.7326 142.788 79.628 123.75 90.0061 116.178C90.767 115.626 91.8503 114.91 92.7645 114.721Z" fill="black"/><path d="M132.355 145.198C137.211 144.109 140.059 148.608 137.713 152.643C136.217 155.222 126.283 159.526 124.841 156.653C123.715 152.88 128.74 146.008 132.355 145.198Z" fill="black"/><path d="M110.192 145.188C116.206 143.872 123.155 155.432 119.847 157.453C115.984 158.085 107.223 154.901 106.657 150.56C106.367 148.308 107.899 145.693 110.198 145.188H110.192Z" fill="black"/></g><defs><clipPath id="logo-clip"><rect width="245" height="237.65" fill="white"/></clipPath></defs></svg>';
1225
+ const LOGO_SVG = '<svg viewBox="0 0 245 238" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#logo-clip)"><path d="M8.75 123.921L21.3889 110.367H32.0833L40.8333 133.602L43.75 155.869L32.0833 158.774L19.4444 155.869L8.75 123.921Z" fill="#FF7F3A"/><path d="M237.225 125.857L224.586 112.303H213.891L205.141 135.539L202.225 157.806L213.891 160.71L226.53 157.806L237.225 125.857Z" fill="#FF7F3A"/><path d="M130.276 57.1198L55.415 78.4187L46.665 88.1V142.315V164.583L68.0539 206.212L93.3317 225.575L117.637 231.384C130.276 230.416 156.332 227.898 159.443 225.575C163.332 222.671 186.665 192.658 190.554 182.977C194.443 173.296 195.415 157.806 195.415 147.156C195.415 136.507 196.387 120.048 195.415 115.208C194.637 111.335 177.591 89.0682 169.165 78.4187L130.276 57.1198Z" fill="#FF7F3A"/><path d="M129.868 30.4824C131.882 28.9302 134.006 26.8043 135.359 24.6522C136.96 22.1107 137.166 19.9586 138.26 17.5486C138.487 17.0434 138.418 16.0226 139.353 16.8172C139.565 16.9961 141.99 20.9373 142.291 21.5214C144.854 26.5781 145.435 32.282 145.943 37.8597C168.385 44.6265 188.639 58.1076 200.824 78.3555C205.004 85.296 209.099 93.9466 210.108 101.971C224.772 94.6044 239.515 103.244 243.626 118.262C250.342 142.793 231.583 169.955 205.421 169.855C204.697 174.096 204.153 178.232 202.975 182.4C196.089 206.684 169.943 227.994 146.101 234.582C99.7856 247.374 45.9189 218.933 39.5938 169.782C13.2839 169.982-5.80773 142.093 1.61651 117.51C5.93895 103.192 20.5496 94.6201 34.5791 101.955C35.3453 101.903 36.3176 96.3197 36.6875 95.1358C41.9188 78.566 53.1846 63.2537 67.0819 52.8614C80.1232 43.1111 99.2202 37.8281 107.247 22.9421C111.099 15.7964 111.564 7.89294 110.333-.0157471C123.083 4.47268 130.724 17.1855 129.863 30.4719L129.868 30.4824ZM121.953 106.181C116.272 96.8144 103.95 89.0267 93.0272 87.4008C79.1774 85.3434 65.745 91.2788 58.8492 103.544C48.8727 121.288 56.6246 142.414 74.9606 150.349C50.8279 179.885 74.8179 211.972 107.157 218.949C130.492 223.985 160.295 216.781 173.69 195.828C183.032 181.216 180.908 163.446 169.98 150.349C195.931 140.352 197.643 103.692 172.971 90.8526C155.042 81.5232 133.636 89.8213 123.041 105.849L121.953 106.181ZM36.9517 154.538C35.9107 151.496 35.1075 148.334 34.4787 145.182C33.4219 139.889 32.328 133.38 32.1801 128.018C32.0057 121.793 35.266 115.552 26.954 113.774C15.5138 111.327 12.3803 123.834 13.2628 132.664C14.4306 144.345 24.3067 156.327 36.957 154.538H36.9517ZM217.876 113.658C216.338 113.968 212.423 116.331 212.195 118.036C212.053 119.12 212.734 120.425 212.761 121.666C212.882 127.481 212.407 133.658 211.53 139.41C210.8 144.193 209.553 149.255 208.063 153.864C207.921 155.969 213.178 154.69 214.436 154.401C222.732 152.512 229.21 144.277 231.055 136.253C233.237 126.734 231.261 110.937 217.871 113.663L217.876 113.658Z" fill="#1a1a1a"/><path d="M158.212 170.413C162.36 169.755 164.738 174.88 161.018 178.48C148.087 190.971 111.162 196.554 95.0185 189.509C91.848 188.125 91.1293 183.589 94.2998 181.958C96.5192 180.816 102.902 183.021 105.904 183.399C119.711 185.136 134.507 183.226 147.104 177.248C149.308 176.206 157.44 170.539 158.206 170.413H158.212Z" fill="#1a1a1a"/><path d="M150.305 114.72C163.975 111.91 165.682 135.542 153.835 137.152C140.884 138.915 139.943 116.846 150.305 114.72Z" fill="#1a1a1a"/><path d="M92.7645 114.721C103.808 112.437 108.141 129.933 99.2058 135.853C88.7326 142.788 79.628 123.75 90.0061 116.178C90.767 115.626 91.8503 114.91 92.7645 114.721Z" fill="#1a1a1a"/><path d="M132.355 145.198C137.211 144.109 140.059 148.608 137.713 152.643C136.217 155.222 126.283 159.526 124.841 156.653C123.715 152.88 128.74 146.008 132.355 145.198Z" fill="#1a1a1a"/><path d="M110.192 145.188C116.206 143.872 123.155 155.432 119.847 157.453C115.984 158.085 107.223 154.901 106.657 150.56C106.367 148.308 107.899 145.693 110.198 145.188H110.192Z" fill="#1a1a1a"/></g><defs><clipPath id="logo-clip"><rect width="245" height="237.65" fill="white"/></clipPath></defs></svg>';
1226
1226
  function generateHtml(jsonData, triage, outputDir) {
1227
1227
  const tests = extractTests(jsonData);
1228
1228
  const isMergedReport = !!jsonData.metadata?.donobuMergedReport;
@@ -1271,7 +1271,9 @@ function generateHtml(jsonData, triage, outputDir) {
1271
1271
  // Group by file
1272
1272
  const uniqueFiles = new Set(tests.map((t) => t.file));
1273
1273
  // --- Build HTML sections ---
1274
- // Build test bar blocks (one square per test, ordered by status)
1274
+ // Pre-assign stable IDs for each test (used by both bar blocks and cards)
1275
+ const testIds = tests.map(() => `t-${uid()}`);
1276
+ // Build test bar blocks (one square per test, ordered by status, clickable)
1275
1277
  const statusOrder = [
1276
1278
  'passed',
1277
1279
  'healed',
@@ -1281,11 +1283,18 @@ function generateHtml(jsonData, triage, outputDir) {
1281
1283
  'skipped',
1282
1284
  ];
1283
1285
  let testBarHtml = '';
1284
- for (const status of statusOrder) {
1285
- const n = counts[status] ?? 0;
1286
- for (let i = 0; i < n; i++) {
1287
- testBarHtml += `<div class="test-bar-block bar-${status.toLowerCase()}" title="${cfg(status).label}"></div>`;
1288
- }
1286
+ // Build an index of tests sorted by status
1287
+ const sortedTestIndices = [...tests.keys()].sort((a, b) => {
1288
+ const sa = statusOrder.indexOf(tests[a].status);
1289
+ const sb = statusOrder.indexOf(tests[b].status);
1290
+ return (sa === -1 ? 99 : sa) - (sb === -1 ? 99 : sb);
1291
+ });
1292
+ for (const idx of sortedTestIndices) {
1293
+ const test = tests[idx];
1294
+ const sc = cfg(test.status);
1295
+ const displayFile = test.file.split('/').pop() ?? test.file;
1296
+ const tooltip = `${displayFile} (${test.specTitle}) — ${sc.label}`;
1297
+ testBarHtml += `<div class="test-bar-block bar-${test.status.toLowerCase()}" data-tooltip="${esc(tooltip)}" data-target="${testIds[idx]}"></div>`;
1289
1298
  }
1290
1299
  // Build stat pills (combined summary + filter controls)
1291
1300
  const statPillEntries = [
@@ -1314,14 +1323,15 @@ function generateHtml(jsonData, triage, outputDir) {
1314
1323
  for (const pill of statPillEntries) {
1315
1324
  const count = counts[pill.key] ?? 0;
1316
1325
  if (count > 0) {
1317
- statPillsHtml += `<button class="stat-pill" data-filter="${pill.key}"><span class="summary-stat-icon ${pill.iconClass}">${pill.icon}</span>${pill.label} (${count})</button>`;
1326
+ statPillsHtml += `<button class="stat-pill" data-filter="${pill.key}">${pill.label}<span class="pill-count">${count}</span></button>`;
1318
1327
  }
1319
1328
  }
1320
1329
  // Flat test list
1321
1330
  let testSectionsHtml = '';
1322
- for (const test of tests) {
1331
+ for (let ti = 0; ti < tests.length; ti++) {
1332
+ const test = tests[ti];
1323
1333
  const sc = cfg(test.status);
1324
- const testId = `t-${uid()}`;
1334
+ const testId = testIds[ti];
1325
1335
  const hasMultipleResults = test.results.length > 1;
1326
1336
  const lastResult = test.results.at(-1);
1327
1337
  const displayFileName = test.file.split('/').pop() ?? test.file;
@@ -1334,15 +1344,24 @@ function generateHtml(jsonData, triage, outputDir) {
1334
1344
  if (test.status === 'failed' || test.status === 'healed') {
1335
1345
  detailsHtml += renderQuickActions(test);
1336
1346
  }
1337
- // 3. Errors + screenshots — raw evidence
1347
+ // 3. Errors + screenshots — raw evidence (side by side when both present)
1338
1348
  if (hasMultipleResults) {
1339
1349
  detailsHtml += renderResultTimeline(test.results, outputDir);
1340
1350
  }
1341
1351
  else if (lastResult) {
1342
- if (lastResult.errors.length) {
1343
- detailsHtml += `<div class="detail-section">${renderErrors(lastResult.errors)}</div>`;
1352
+ const errHtml = lastResult.errors.length
1353
+ ? renderErrors(lastResult.errors)
1354
+ : '';
1355
+ const attHtml = renderAttachments(lastResult.attachments, outputDir, lastResult.stepScreenshots);
1356
+ if (errHtml && attHtml) {
1357
+ detailsHtml += `<div class="evidence-row"><div class="evidence-col evidence-col-code">${errHtml}</div><div class="evidence-col evidence-col-img">${attHtml}</div></div>`;
1358
+ }
1359
+ else if (errHtml) {
1360
+ detailsHtml += `<div class="detail-section">${errHtml}</div>`;
1361
+ }
1362
+ else if (attHtml) {
1363
+ detailsHtml += attHtml;
1344
1364
  }
1345
- detailsHtml += renderAttachments(lastResult.attachments, outputDir, lastResult.stepScreenshots);
1346
1365
  }
1347
1366
  // 4. Self-healed banner
1348
1367
  if (test.isSelfHealed) {
@@ -1367,9 +1386,14 @@ function generateHtml(jsonData, triage, outputDir) {
1367
1386
  const hasDetails = detailsHtml.length > 0;
1368
1387
  const expandableClass = hasDetails ? 'expandable' : '';
1369
1388
  const chevron = hasDetails
1370
- ? '<span class="chevron">&#9656;</span>'
1389
+ ? '<span class="chevron"></span>'
1371
1390
  : '<span class="chevron-spacer"></span>';
1372
1391
  const totalTestDuration = test.results.reduce((s, r) => s + r.duration, 0);
1392
+ const totalStepCount = test.results.reduce((s, r) => s + r.stepScreenshots.length, 0);
1393
+ // Flow ID line goes inside the detail section (not on the summary row)
1394
+ const flowIdDetailHtml = test.flowId
1395
+ ? `<div class="flow-id-detail"><span class="detail-label">Flow ID</span><span class="flow-id-value">${esc(test.flowId)}<button class="copy-flow-id" data-flow-id="${esc(test.flowId)}" title="Copy flow ID"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button></span></div>`
1396
+ : '';
1373
1397
  testSectionsHtml += `
1374
1398
  <div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" data-status="${test.status}" ${hasDetails ? `data-detail="${testId}"` : ''}>
1375
1399
  <div class="test-summary">
@@ -1377,12 +1401,12 @@ function generateHtml(jsonData, triage, outputDir) {
1377
1401
  <span class="status-dot" style="background:${sc.color}" title="${sc.label}"></span>
1378
1402
  <div class="test-name-group">
1379
1403
  <span class="test-name"><span class="test-file">${esc(displayFileName)}</span> (${esc(test.specTitle)})</span>
1380
- ${test.flowId ? `<span class="flow-id">Flow ID: ${esc(test.flowId)}<button class="copy-flow-id" data-flow-id="${esc(test.flowId)}" title="Copy flow ID"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button></span>` : ''}
1381
1404
  </div>
1382
1405
  ${test.plan ? `<span class="inline-reason" style="color:${reasonCfg(test.plan.plan.failureReason).color}" title="${esc(test.plan.plan.failureReason)}">${esc(reasonCfg(test.plan.plan.failureReason).label)}</span>` : ''}
1406
+ ${totalStepCount > 0 ? `<span class="test-step-count" title="${totalStepCount} steps">${totalStepCount} steps</span>` : ''}
1383
1407
  <span class="test-duration">${fmtDuration(totalTestDuration)}</span>
1384
1408
  </div>
1385
- ${hasDetails ? `<div class="test-detail" id="${testId}">${detailsHtml}</div>` : ''}
1409
+ ${hasDetails ? `<div class="test-detail" id="${testId}">${flowIdDetailHtml}${detailsHtml}</div>` : ''}
1386
1410
  </div>`;
1387
1411
  }
1388
1412
  const mergedBanner = isMergedReport
@@ -1397,21 +1421,41 @@ function generateHtml(jsonData, triage, outputDir) {
1397
1421
  <meta charset="UTF-8">
1398
1422
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1399
1423
  <title>Donobu Test Report</title>
1424
+ <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700&display=swap" rel="stylesheet">
1400
1425
  <link rel="icon" type="image/png" href="data:image/png;base64,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">
1401
1426
  <style>
1402
1427
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
1403
1428
  :root{
1404
- --bg:#0f1117;--surface:#161922;--surface-raised:#1e2130;
1405
- --border:#2a2d3a;--border-subtle:#222536;
1429
+ --bg:#191919;--surface:#212121;--surface-raised:#2a2a2a;
1430
+ --border:#333;--border-subtle:#2a2a2a;
1406
1431
  --text:#e4e5ea;--text-muted:#8b8d98;--text-dim:#5f6170;
1407
1432
  --green:#10b981;--red:#ef4444;--purple:#8b5cf6;
1408
1433
  --amber:#f59e0b;--orange:#f97316;--gray:#6b7280;
1409
1434
  --accent:#FF7F3A;
1410
1435
  --radius:8px;--radius-lg:12px;
1436
+ --font:'Inter Tight',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;
1411
1437
  --mono:'SF Mono','Fira Code','Fira Mono',Menlo,Consolas,monospace;
1438
+ --overlay-light:rgba(255,255,255,.04);--overlay-light-hover:rgba(255,255,255,.06);--overlay-light-active:rgba(255,255,255,.12);
1439
+ --bar-pass:#2d8a6e;--bar-fail:#b44a4a;--bar-heal:#7352b5;--bar-warn:#a8832e;--bar-skip:#4a4a4f;
1440
+ --dot-pass:#2d8a6e;--dot-fail:#b44a4a;--dot-heal:#7352b5;--dot-warn:#a8832e;--dot-skip:#4a4a4f;
1441
+ }
1442
+ @media(prefers-color-scheme:light){
1443
+ :root{
1444
+ --bg:#f8f8fa;--surface:#ffffff;--surface-raised:#f2f2f4;
1445
+ --border:#c8c8cd;--border-subtle:#dcdce0;
1446
+ --text:#1a1a2e;--text-muted:#3f3f46;--text-dim:#71717a;
1447
+ --green:#059669;--red:#dc2626;--purple:#7c3aed;
1448
+ --amber:#d97706;--orange:#ea580c;--gray:#6b7280;
1449
+ --accent:#e8621a;
1450
+ --overlay-light:rgba(0,0,0,.03);--overlay-light-hover:rgba(0,0,0,.04);--overlay-light-active:rgba(0,0,0,.08);
1451
+ --bar-pass:var(--green);--bar-fail:var(--red);--bar-heal:var(--purple);--bar-warn:var(--amber);--bar-skip:var(--gray);
1452
+ --dot-pass:var(--green);--dot-fail:var(--red);--dot-heal:var(--purple);--dot-warn:var(--amber);--dot-skip:var(--gray);
1453
+ }
1412
1454
  }
1413
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background:var(--bg);color:var(--text);line-height:1.5;padding:0}
1414
- .container{max-width:1100px;margin:0 auto;padding:32px 24px}
1455
+ body{font-family:var(--font);background:var(--bg);color:var(--text);line-height:1.5;padding:0;position:relative;min-height:100vh}
1456
+ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(-50%);width:900px;height:800px;border-radius:50%;background:rgba(255,127,58,.15);filter:blur(256px);pointer-events:none;z-index:0}
1457
+ @media(prefers-color-scheme:light){body::before{top:-780px;background:rgba(255,127,58,.06);filter:blur(300px)}}
1458
+ .container{max-width:1100px;margin:0 auto;padding:32px 24px;position:relative;z-index:1}
1415
1459
 
1416
1460
  /* Header */
1417
1461
  .report-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
@@ -1424,24 +1468,24 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
1424
1468
  .summary-card{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);padding:20px 24px;margin:20px 0 16px}
1425
1469
  .summary-sub{font-size:13px;color:var(--text-muted);margin-bottom:14px}
1426
1470
  .test-bar{display:flex;gap:4px;margin-bottom:16px;flex-wrap:wrap}
1427
- .test-bar-block{width:28px;height:28px;border-radius:4px;flex-shrink:0;transition:transform .1s}
1471
+ .test-bar-block{width:28px;height:28px;border-radius:4px;flex-shrink:0;transition:transform .1s;cursor:pointer;position:relative}
1428
1472
  .test-bar-block:hover{transform:scale(1.15)}
1429
- .test-bar-block.bar-passed{background:var(--accent)}
1430
- .test-bar-block.bar-healed{background:var(--purple)}
1431
- .test-bar-block.bar-failed,.test-bar-block.bar-timedout,.test-bar-block.bar-interrupted{background:rgba(255,255,255,.12)}
1432
- .test-bar-block.bar-skipped{background:rgba(255,255,255,.06)}
1433
- .summary-stats{display:flex;align-items:center;gap:8px}
1473
+ .test-bar-block[data-tooltip]:hover::after{content:attr(data-tooltip);position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:var(--radius);padding:6px 10px;font-size:11px;font-family:var(--font);white-space:nowrap;z-index:10;pointer-events:none;box-shadow:0 4px 12px rgba(0,0,0,.2)}
1474
+ .test-bar-block[data-tooltip]:hover::before{content:'';position:absolute;top:calc(100% + 2px);left:50%;transform:translateX(-50%);border:5px solid transparent;border-bottom-color:var(--border);z-index:10;pointer-events:none}
1475
+ .test-bar-block.bar-passed{background:var(--bar-pass)}
1476
+ .test-bar-block.bar-healed{background:var(--bar-heal)}
1477
+ .test-bar-block.bar-failed{background:var(--bar-fail)}
1478
+ .test-bar-block.bar-timedout,.test-bar-block.bar-interrupted{background:var(--bar-warn)}
1479
+ .test-bar-block.bar-skipped{background:var(--bar-skip)}
1480
+ .summary-stats{display:flex;align-items:center;gap:8px;padding:12px 0}
1434
1481
  .stat-pills{display:flex;gap:6px;flex-wrap:wrap;flex:1}
1435
- .stat-pill{display:flex;align-items:center;gap:6px;background:none;border:1px solid var(--border-subtle);border-radius:var(--radius);padding:6px 14px;cursor:pointer;font-size:14px;font-weight:600;font-family:inherit;color:var(--text-muted);transition:background .15s,border-color .15s,color .15s}
1436
- .stat-pill:hover{background:var(--surface-raised);border-color:var(--border)}
1437
- .stat-pill.active{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
1438
- .summary-stat-icon{width:22px;height:22px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0}
1439
- .summary-stat-icon.icon-pass{background:rgba(16,185,129,.15);color:var(--green)}
1440
- .summary-stat-icon.icon-heal{background:rgba(139,92,246,.15);color:var(--purple)}
1441
- .summary-stat-icon.icon-fail{background:rgba(239,68,68,.15);color:var(--red)}
1442
- .summary-stat-icon.icon-skip{background:rgba(107,114,128,.15);color:var(--gray)}
1443
- .clear-filter{background:none;border:1px solid var(--border-subtle);color:var(--text-dim);padding:6px 12px;border-radius:var(--radius);cursor:pointer;font-size:12px;font-family:inherit;display:none;align-items:center;gap:4px;flex-shrink:0;transition:color .15s,border-color .15s}
1444
- .clear-filter:hover{color:var(--text);border-color:var(--text-muted)}
1482
+ .stat-pill{display:flex;align-items:center;gap:8px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:6px 14px;cursor:pointer;font-size:13px;font-weight:600;font-family:inherit;color:var(--text-muted);transition:all .2s}
1483
+ .stat-pill:hover{background:var(--surface-raised);border-color:var(--text-dim)}
1484
+ .stat-pill.active{background:var(--accent);border-color:var(--accent);color:#fff}
1485
+ .stat-pill.active .pill-count{background:rgba(255,255,255,.25);color:#fff}
1486
+ .pill-count{font-size:11px;font-weight:700;background:var(--overlay-light-active);color:var(--text-dim);padding:1px 7px;border-radius:calc(var(--radius) - 2px);min-width:20px;text-align:center;transition:all .2s}
1487
+ .clear-filter{background:var(--surface);border:1px solid var(--border);color:var(--text-muted);padding:6px 14px;border-radius:var(--radius);cursor:pointer;font-size:12px;font-weight:500;font-family:inherit;display:none;align-items:center;gap:5px;flex-shrink:0;transition:all .2s}
1488
+ .clear-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
1445
1489
  .clear-filter.visible{display:flex}
1446
1490
 
1447
1491
  /* Test cards */
@@ -1451,27 +1495,38 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
1451
1495
  .test-summary{display:flex;align-items:center;gap:10px;padding:14px 16px;transition:background .1s}
1452
1496
  .test-card.expandable .test-summary{cursor:pointer}
1453
1497
  .test-card.expandable .test-summary:hover{background:var(--surface-raised)}
1454
- .chevron{font-size:11px;color:var(--text-dim);transition:transform .15s;flex-shrink:0;width:12px}
1455
- .chevron-spacer{width:12px;flex-shrink:0}
1498
+ .chevron{width:0;height:0;border-style:solid;border-width:5px 0 5px 8px;border-color:transparent transparent transparent var(--text-muted);flex-shrink:0;transition:transform .2s;margin:0 5px}
1499
+ .chevron-spacer{width:18px;flex-shrink:0}
1456
1500
  .test-card.expanded .chevron{transform:rotate(90deg)}
1457
1501
  .status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
1458
1502
  .test-file{color:var(--text);font-weight:600}
1459
1503
  .test-name-group{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
1460
1504
  .test-name{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1461
1505
  .flow-id{font-size:11px;color:var(--text-dim);opacity:.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;align-items:center;gap:4px}
1462
- .copy-flow-id{background:rgba(255,255,255,.06);border:none;border-radius:4px;cursor:pointer;padding:3px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);transition:background .15s,color .15s;flex-shrink:0}
1463
- .copy-flow-id:hover{background:rgba(255,255,255,.12);color:#fff}
1506
+ .copy-flow-id{background:var(--overlay-light-hover);border:none;border-radius:4px;cursor:pointer;padding:3px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);transition:background .15s,color .15s;flex-shrink:0}
1507
+ .copy-flow-id:hover{background:var(--overlay-light-active);color:var(--text)}
1464
1508
  .copy-flow-id svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
1465
1509
  .copy-flow-id .check-icon{color:#22c55e}
1466
- .inline-reason{font-size:11px;font-weight:500;flex-shrink:0;padding:1px 8px;border-radius:4px;background:rgba(255,255,255,.04)}
1510
+ .inline-reason{font-size:11px;font-weight:500;flex-shrink:0;padding:1px 8px;border-radius:4px;background:var(--overlay-light)}
1511
+ .test-step-count{font-size:11px;color:var(--text-dim);flex-shrink:0;font-family:var(--mono);padding:2px 8px;background:rgba(255,255,255,.04);border-radius:4px}
1467
1512
  .test-duration{font-size:12px;color:var(--text-dim);flex-shrink:0;font-family:var(--mono)}
1513
+ .flow-id-detail{display:flex;align-items:center;gap:10px;margin-bottom:4px;padding:8px 12px;background:var(--surface-raised);border:1px solid var(--border-subtle);border-radius:var(--radius)}
1514
+ .flow-id-detail .detail-label{margin-bottom:0;font-size:10px;letter-spacing:.8px}
1515
+ .flow-id-value{font-size:12px;color:var(--text-dim);font-family:var(--mono);display:flex;align-items:center;gap:6px}
1468
1516
 
1469
1517
  /* Test detail */
1470
- .test-detail{padding:0 16px 14px 50px;display:none;flex-direction:column;gap:10px}
1518
+ .test-detail{padding:10px 20px 16px 50px;display:none;flex-direction:column;gap:10px}
1471
1519
  .test-card.expanded .test-detail{display:flex}
1472
1520
  .detail-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);margin-bottom:6px}
1473
1521
  .detail-objective{font-size:13px;padding:10px 14px;background:var(--bg);border-radius:var(--radius);border:1px solid var(--border-subtle);white-space:pre-wrap;word-break:break-word}
1474
1522
 
1523
+ /* Evidence row — errors + screenshot side by side */
1524
+ .evidence-row{display:flex;gap:16px;align-items:flex-start}
1525
+ .evidence-col-code{flex:1;min-width:0}
1526
+ .evidence-col-img{flex-shrink:0;max-width:50%}
1527
+ .evidence-col-img .screenshot{max-height:400px}
1528
+ @media(max-width:768px){.evidence-row{flex-direction:column}.evidence-col-img{max-width:100%}}
1529
+
1475
1530
  /* Failure summary banner — the prominent "why did this fail?" block */
1476
1531
  .failure-summary-banner{background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.18);border-radius:var(--radius-lg);padding:14px 16px}
1477
1532
  .failure-summary-header{display:flex;align-items:center;gap:8px;margin-bottom:6px}
@@ -1524,11 +1579,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
1524
1579
  .filmstrip-summary{font-size:11px;color:var(--text-dim);margin-top:2px;padding-left:22px}
1525
1580
  .step-status-ok{color:var(--green);font-size:12px;font-weight:bold}
1526
1581
  .step-status-fail{color:var(--red);font-size:12px;font-weight:bold}
1527
- .filmstrip-detail{display:none;padding:8px 0 4px 22px;flex-direction:column;gap:8px}
1582
+ .filmstrip-detail{display:none;padding:8px 0 4px 22px;flex-direction:row;gap:12px;align-items:flex-start}
1528
1583
  .filmstrip-step.open .filmstrip-detail{display:flex}
1584
+ .filmstrip-detail>a{flex-shrink:0;max-width:50%}
1585
+ .filmstrip-detail>.step-json-wrap{flex:1;min-width:0}
1529
1586
  .step-json-wrap{position:relative}
1530
- .step-json-wrap .copy-json{position:absolute;top:6px;right:6px;background:rgba(255,255,255,.06);border:none;border-radius:4px;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);transition:background .15s,color .15s;z-index:1}
1531
- .step-json-wrap .copy-json:hover{background:rgba(255,255,255,.12);color:#fff}
1587
+ .step-json-wrap .copy-json{position:absolute;top:6px;right:6px;background:var(--overlay-light-hover);border:none;border-radius:4px;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);transition:background .15s,color .15s;z-index:1}
1588
+ .step-json-wrap .copy-json:hover{background:var(--overlay-light-active);color:var(--text)}
1532
1589
  .step-json-wrap .copy-json svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
1533
1590
  .step-json-wrap .copy-json .check-icon{color:#22c55e}
1534
1591
  .step-json{font-size:11px;font-family:var(--mono);background:var(--bg);border:1px solid var(--border-subtle);border-radius:var(--radius);padding:8px 12px;margin:0;color:var(--text-muted);overflow-x:auto;max-height:300px;overflow-y:auto}
@@ -1686,8 +1743,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
1686
1743
  .qa-label{font-size:12px;color:var(--text-muted);white-space:nowrap;min-width:140px}
1687
1744
  .qa-cmd-wrapper{display:flex;align-items:center;gap:6px;flex:1;min-width:0}
1688
1745
  .qa-cmd{font-size:12px;font-family:var(--mono);background:var(--bg-card);padding:5px 10px;border-radius:4px;border:1px solid var(--border-subtle);color:var(--text);white-space:nowrap;overflow-x:auto;flex:1;min-width:0}
1689
- .qa-copy{background:rgba(255,255,255,.06);border:none;border-radius:4px;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);transition:background .15s,color .15s;flex-shrink:0}
1690
- .qa-copy:hover{background:rgba(255,255,255,.12);color:#fff}
1746
+ .qa-copy{background:var(--overlay-light-hover);border:none;border-radius:4px;cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;color:var(--text-dim);transition:background .15s,color .15s;flex-shrink:0}
1747
+ .qa-copy:hover{background:var(--overlay-light-active);color:var(--text)}
1691
1748
  .qa-copy svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
1692
1749
  .qa-copy .check-icon{color:#22c55e}
1693
1750
  .qa-source{font-size:12px;color:var(--text-dim);margin-top:6px;padding-top:6px;border-top:1px solid var(--border-subtle)}
@@ -1695,8 +1752,9 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
1695
1752
 
1696
1753
  /* Footer */
1697
1754
  .report-footer{display:flex;align-items:center;justify-content:space-between;font-size:12px;color:var(--text-dim);margin-top:32px;padding-top:16px;border-top:1px solid var(--border-subtle)}
1698
- .footer-brand{display:flex;align-items:center;gap:8px;font-weight:600;color:var(--text-muted)}
1699
- .footer-brand svg{width:20px;height:20px}
1755
+ .footer-brand{display:inline-flex;align-items:center;gap:6px;font-weight:600;color:var(--text-muted);text-decoration:none}
1756
+ .footer-brand .logo{display:inline-flex;align-items:center}
1757
+ .footer-brand svg{width:18px;height:18px;vertical-align:middle}
1700
1758
 
1701
1759
  /* Lightbox */
1702
1760
  .lightbox{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:1000;align-items:center;justify-content:center;cursor:zoom-out}
@@ -1790,6 +1848,9 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
1790
1848
  if(pill){filterByStatus(pill.getAttribute('data-filter'));return}
1791
1849
  // Clear filter
1792
1850
  if(e.target.closest('[data-clear-filter]')){clearFilter();return}
1851
+ // Test bar block click — scroll to and highlight the target test card
1852
+ var barBlock=e.target.closest('.test-bar-block[data-target]');
1853
+ if(barBlock){var tid=barBlock.getAttribute('data-target');var card=document.getElementById(tid);if(card){var tc=card.closest('.test-card');if(tc){tc.classList.add('expanded');tc.scrollIntoView({behavior:'smooth',block:'center'});tc.style.outline='2px solid var(--accent)';setTimeout(function(){tc.style.outline=''},1500)}}return}
1793
1854
  // Filmstrip step expand (skip if clicking a link inside)
1794
1855
  var step=e.target.closest('.filmstrip-step.expandable');
1795
1856
  if(step&&!e.target.closest('a')&&!e.target.closest('.audit-check')){step.classList.toggle('open');return}