Skip to content

Commit 410b1e7

Browse files
authored
Merge pull request #1769 from finos/conformance-runner
Custom mocha conformance test reporter
2 parents ce8b095 + 697140b commit 410b1e7

File tree

4 files changed

+220
-0
lines changed

4 files changed

+220
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1717
* Ported FDC3 Conformance Project as-is into the FDC3 Monorepo, just including minimal fixes for typescript compilation. ([#1576](https://github.com/finos/FDC3/pull/1576))
1818
* Added `clearContext` function and associated `contextClearedEvent` to the `Channel` API, to be able to clear specific or all context types from the channel. ([#1379](https://github.com/finos/FDC3/pull/1379))
1919
* Added Conformance tests for FDC3 2.2 ([#1586](https://github.com/finos/FDC3/pull/1586))
20+
* Added custom mocha test runner for conformance tests to better display test progress. ([#1769](https://github.com/finos/FDC3/pull/1769))
2021

2122
### Changed
2223

toolbox/fdc3-conformance/src/test/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
export * from './testSuite';
22
import { getAgent } from '@finos/fdc3';
33
import { getPackMembers, getPackNames, executeTestsInBrowser, executeManualTestsInBrowser } from './testSuite';
4+
import { ProgressReporter } from './progressReporter';
45

56
// eslint-disable-next-line @typescript-eslint/no-require-imports
67
require('mocha/mocha.css');
78
// eslint-disable-next-line @typescript-eslint/no-require-imports
89
require('source-map-support/browser-source-map-support.js');
910

1011
mocha.setup('bdd');
12+
mocha.reporter(ProgressReporter);
1113
const testSuite = document.getElementById('testSuite')!;
1214

1315
// populate drop-down
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* Custom Mocha reporter that shows an amber in-progress indicator when each test
3+
* starts, then updates the indicator to a green tick or red cross when the test completes.
4+
*/
5+
export class ProgressReporter extends Mocha.reporters.Base {
6+
private testElements = new Map<string, HTMLElement>();
7+
private suiteStack: HTMLElement[];
8+
private canvas: HTMLCanvasElement;
9+
private passCount: HTMLElement;
10+
private failCount: HTMLElement;
11+
private durationCount: HTMLElement;
12+
private startTime: number;
13+
private durationTimer: ReturnType<typeof setInterval>;
14+
15+
constructor(runner: Mocha.Runner, options?: Mocha.MochaOptions) {
16+
super(runner, options);
17+
18+
const root = document.getElementById('mocha')!;
19+
root.replaceChildren();
20+
21+
const statsEl = document.createElement('ul');
22+
statsEl.id = 'mocha-stats';
23+
root.appendChild(statsEl);
24+
25+
const report = document.createElement('ul');
26+
report.id = 'mocha-report';
27+
root.appendChild(report);
28+
29+
// Progress ring canvas
30+
const progressLi = document.createElement('li');
31+
progressLi.className = 'progress-ring';
32+
this.canvas = document.createElement('canvas');
33+
this.canvas.width = 40;
34+
this.canvas.height = 40;
35+
progressLi.appendChild(this.canvas);
36+
statsEl.appendChild(progressLi);
37+
38+
const passLi = document.createElement('li');
39+
passLi.className = 'passes';
40+
this.passCount = this.buildStatItem(passLi, 'passes: ', '0');
41+
statsEl.appendChild(passLi);
42+
43+
const failLi = document.createElement('li');
44+
failLi.className = 'failures';
45+
this.failCount = this.buildStatItem(failLi, 'failures: ', '0');
46+
statsEl.appendChild(failLi);
47+
48+
const durationLi = document.createElement('li');
49+
durationLi.className = 'duration';
50+
this.durationCount = this.buildStatItem(durationLi, 'duration: ', '0', 's');
51+
statsEl.appendChild(durationLi);
52+
53+
this.suiteStack = [report];
54+
this.startTime = Date.now();
55+
56+
this.durationTimer = setInterval(() => this.updateDuration(), 100);
57+
58+
runner.on('suite', suite => this.onSuite(suite));
59+
runner.on('suite end', suite => this.onSuiteEnd(suite));
60+
runner.on('test', test => this.onTest(test));
61+
runner.on('pass', test => this.onPass(test));
62+
runner.on('fail', (test, err) => this.onFail(test, err));
63+
runner.on('end', () => this.onEnd());
64+
}
65+
66+
private onSuite(suite: Mocha.Suite) {
67+
if (suite.root) return;
68+
const li = document.createElement('li');
69+
li.className = 'suite';
70+
const h1 = document.createElement('h1');
71+
const a = document.createElement('a');
72+
a.textContent = suite.title;
73+
h1.appendChild(a);
74+
li.appendChild(h1);
75+
const ul = document.createElement('ul');
76+
li.appendChild(ul);
77+
this.suiteStack[this.suiteStack.length - 1].appendChild(li);
78+
this.suiteStack.push(ul);
79+
}
80+
81+
private onSuiteEnd(suite: Mocha.Suite) {
82+
if (suite.root) return;
83+
if (this.suiteStack.length > 1) this.suiteStack.pop();
84+
}
85+
86+
private onTest(test: Mocha.Test) {
87+
const li = document.createElement('li');
88+
li.className = 'test running';
89+
const h2 = document.createElement('h2');
90+
h2.textContent = test.title;
91+
li.appendChild(h2);
92+
this.suiteStack[this.suiteStack.length - 1].appendChild(li);
93+
this.testElements.set(test.fullTitle(), li);
94+
li.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
95+
}
96+
97+
private onPass(test: Mocha.Test) {
98+
const li = this.testElements.get(test.fullTitle());
99+
if (li) {
100+
li.className = `test pass ${this.getSpeedClass(test)}`;
101+
this.addDuration(li, test);
102+
}
103+
this.updateStats();
104+
}
105+
106+
private onFail(test: Mocha.Test, err: Error) {
107+
const li = this.testElements.get(test.fullTitle());
108+
if (li) {
109+
li.className = 'test fail';
110+
this.addDuration(li, test);
111+
const pre = document.createElement('pre');
112+
pre.className = 'error';
113+
pre.textContent = err.message;
114+
li.appendChild(pre);
115+
}
116+
this.updateStats();
117+
}
118+
119+
private onEnd() {
120+
clearInterval(this.durationTimer);
121+
this.updateDuration();
122+
}
123+
124+
private getSpeedClass(test: Mocha.Test): string {
125+
const slow = test.slow ? test.slow() : 75;
126+
const duration = test.duration ?? 0;
127+
if (duration > slow) return 'slow';
128+
if (duration > slow / 2) return 'medium';
129+
return 'fast';
130+
}
131+
132+
private addDuration(li: HTMLElement, test: Mocha.Test) {
133+
if (test.duration !== undefined) {
134+
const h2 = li.querySelector('h2')!;
135+
const span = document.createElement('span');
136+
span.className = 'duration';
137+
span.textContent = ` ${test.duration}ms`;
138+
h2.appendChild(span);
139+
}
140+
}
141+
142+
private buildStatItem(parent: HTMLElement, label: string, initialValue: string, suffix?: string): HTMLElement {
143+
const a = document.createElement('a');
144+
a.href = 'javascript:void(0);';
145+
a.textContent = label;
146+
const em = document.createElement('em');
147+
em.textContent = initialValue;
148+
a.appendChild(em);
149+
if (suffix) {
150+
a.appendChild(document.createTextNode(suffix));
151+
}
152+
parent.appendChild(a);
153+
return em;
154+
}
155+
156+
private updateDuration() {
157+
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(2);
158+
this.durationCount.textContent = elapsed;
159+
}
160+
161+
private updateStats() {
162+
const { passes, failures } = this.stats;
163+
this.passCount.textContent = String(passes);
164+
this.failCount.textContent = String(failures);
165+
166+
// Draw progress ring
167+
const total = this.runner.total;
168+
const completed = passes + failures;
169+
const percent = total > 0 ? completed / total : 0;
170+
const ctx = this.canvas.getContext('2d')!;
171+
const x = this.canvas.width / 2;
172+
const y = this.canvas.height / 2;
173+
const rad = Math.min(x, y) - 1;
174+
175+
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
176+
177+
// Background circle
178+
ctx.beginPath();
179+
ctx.arc(x, y, rad, 0, Math.PI * 2, false);
180+
ctx.strokeStyle = '#9f9f9f';
181+
ctx.lineWidth = 2;
182+
ctx.stroke();
183+
184+
// Progress arc
185+
ctx.beginPath();
186+
ctx.arc(x, y, rad - 1, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * percent, false);
187+
ctx.strokeStyle = '#4fbc5f';
188+
ctx.lineWidth = 3;
189+
ctx.stroke();
190+
191+
// Percentage text
192+
ctx.fillStyle = '#888';
193+
ctx.font = '11px "Helvetica Neue", Helvetica, Arial, sans-serif';
194+
ctx.textAlign = 'center';
195+
ctx.textBaseline = 'middle';
196+
ctx.fillText(`${Math.round(percent * 100)}%`, x, y);
197+
}
198+
}

toolbox/fdc3-conformance/static/lib/index.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,22 @@ body {
4242
.bold {
4343
font-weight: bold;
4444
}
45+
46+
#mocha-stats .duration em {
47+
min-width: 4em;
48+
display: inline-block;
49+
text-align: right;
50+
}
51+
52+
#mocha .test.running::before {
53+
content: '●';
54+
font-size: 12px;
55+
display: block;
56+
float: left;
57+
margin-right: 5px;
58+
color: #daa520;
59+
}
60+
61+
#mocha li.suite {
62+
padding: 0px 0px 25px 0px;
63+
}

0 commit comments

Comments
 (0)