Skip to content

Commit de0e16c

Browse files
authored
feat(elements): add progress bar for real-time reporting (#2560)
Add a result bar that fills up during real-time reporting. It is actually always visible, also for static reports.
1 parent f7c6f5d commit de0e16c

File tree

44 files changed

+339
-29
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+339
-29
lines changed

packages/elements/src/components/app/app.component.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import { MutantResult, MutationTestResult } from 'mutation-testing-report-schema
55
import { MetricsResult, MutantModel, TestModel, calculateMutationTestMetrics } from 'mutation-testing-metrics';
66
import { tailwind, globals } from '../../style';
77
import { locationChange$, View } from '../../lib/router';
8-
import { Subscription, debounceTime, fromEvent } from 'rxjs';
8+
import { Subscription, fromEvent, sampleTime } from 'rxjs';
99
import theme from './theme.scss';
1010
import { createCustomEvent } from '../../lib/custom-events';
1111
import { FileUnderTestModel, Metrics, MutationTestMetricsResult, TestFileModel, TestMetrics } from 'mutation-testing-metrics';
1212
import { toAbsoluteUrl } from '../../lib/html-helpers';
1313
import { isLocalStorageAvailable } from '../../lib/browser';
1414
import { mutantChanges } from '../../lib/mutant-changes';
15-
import { RealtimeElement } from '../realtime-element';
15+
import { RealTimeElement } from '../real-time-element';
1616

1717
interface BaseContext {
1818
path: string[];
@@ -40,7 +40,7 @@ const UPDATE_CYCLE_TIME = 100;
4040
type Context = MutantContext | TestContext;
4141

4242
@customElement('mutation-test-report-app')
43-
export class MutationTestReportAppComponent extends RealtimeElement {
43+
export class MutationTestReportAppComponent extends RealTimeElement {
4444
@property({ attribute: false })
4545
public report: MutationTestResult | undefined;
4646

@@ -280,7 +280,7 @@ export class MutationTestReportAppComponent extends RealtimeElement {
280280
});
281281

282282
const applySubscription = fromEvent(this.source, 'mutant-tested')
283-
.pipe(debounceTime(UPDATE_CYCLE_TIME))
283+
.pipe(sampleTime(UPDATE_CYCLE_TIME))
284284
.subscribe(() => {
285285
this.applyChanges();
286286
});
@@ -325,10 +325,18 @@ export class MutationTestReportAppComponent extends RealtimeElement {
325325
<div class="container bg-white pb-4 font-sans text-gray-800 motion-safe:transition-max-width">
326326
<div class="space-y-4 transition-colors">
327327
${this.renderErrorMessage()}
328-
<mte-theme-switch @theme-switch="${this.themeSwitch}" class="sticky top-offset z-20 float-right mx-4 pt-4" .theme="${this.theme}">
328+
<mte-theme-switch @theme-switch="${this.themeSwitch}" class="sticky top-offset z-20 float-right pt-6" .theme="${this.theme}">
329329
</mte-theme-switch>
330330
${this.renderTitle()} ${this.renderTabs()}
331-
<mte-breadcrumb .view="${this.context.view}" class="my-4" .path="${this.context.path}"></mte-breadcrumb>
331+
<mte-breadcrumb .view="${this.context.view}" .path="${this.context.path}"></mte-breadcrumb>
332+
<mte-result-status-bar
333+
.detected="${this.rootModel?.systemUnderTestMetrics.metrics.totalDetected}"
334+
.undetected="${this.rootModel?.systemUnderTestMetrics.metrics.totalUndetected}"
335+
.invalid="${this.rootModel?.systemUnderTestMetrics.metrics.totalInvalid}"
336+
.ignored="${this.rootModel?.systemUnderTestMetrics.metrics.ignored}"
337+
.pending="${this.rootModel?.systemUnderTestMetrics.metrics.pending}"
338+
.total="${this.rootModel?.systemUnderTestMetrics.metrics.totalMutants}"
339+
></mte-result-status-bar>
332340
${this.context.view === 'mutant' && this.context.result
333341
? html`<mte-mutant-view
334342
id="mte-mutant-view"

packages/elements/src/components/breadcrumb.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class MutationTestReportBreadcrumbComponent extends LitElement {
2525
}
2626

2727
public render() {
28-
return html`<nav class="my-6 flex rounded-md border border-gray-200 bg-primary-100 px-5 py-3 text-gray-700" aria-label="Breadcrumb">
28+
return html`<nav class="my-4 flex rounded-md border border-primary-600 bg-primary-100 p-3 text-gray-700" aria-label="Breadcrumb">
2929
<ol class="inline-flex items-center">
3030
${this.path && this.path.length > 0 ? this.renderLink(this.rootName, []) : this.renderActiveItem(this.rootName)}
3131
${this.renderBreadcrumbItems()}
@@ -53,7 +53,7 @@ export class MutationTestReportBreadcrumbComponent extends LitElement {
5353

5454
private renderActiveItem(title: string) {
5555
return html`<li aria-current="page">
56-
<span class="ml-1 text-sm font-medium text-gray-800 md:ml-2">${title}</span>
56+
<span class="ml-1 text-sm font-medium text-gray-800">${title}</span>
5757
</li> `;
5858
}
5959

packages/elements/src/components/drawer-mutant/drawer-mutant.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import { tailwind } from '../../style';
77
import { DrawerMode } from '../drawer/drawer.component';
88
import { renderDrawer } from '../drawer/util';
99
import { renderDetailLine, renderEmoji, renderSummaryContainer, renderSummaryLine } from './util';
10-
import { RealtimeElement } from '../realtime-element';
10+
import { RealTimeElement } from '../real-time-element';
1111

1212
const describeTest = (test: TestModel) => `${test.name}${test.sourceFile && test.location ? ` (${describeLocation(test)})` : ''}`;
1313

1414
@customElement('mte-drawer-mutant')
15-
export class MutationTestReportDrawerMutant extends RealtimeElement {
15+
export class MutationTestReportDrawerMutant extends RealTimeElement {
1616
@property()
1717
public mutant?: MutantModel;
1818

packages/elements/src/components/drawer-test/drawer-test.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { tailwind } from '../../style';
66
import { renderDetailLine, renderEmoji, renderSummaryContainer, renderSummaryLine } from '../drawer-mutant/util';
77
import { DrawerMode } from '../drawer/drawer.component';
88
import { renderDrawer } from '../drawer/util';
9-
import { RealtimeElement } from '../realtime-element';
9+
import { RealTimeElement } from '../real-time-element';
1010

1111
const describeMutant = (mutant: MutantModel) => html`<code>${mutant.getMutatedLines()}</code> (${describeLocation(mutant)})`;
1212

1313
@customElement('mte-drawer-test')
14-
export class MutationTestReportDrawerTestComponent extends RealtimeElement {
14+
export class MutationTestReportDrawerTestComponent extends RealTimeElement {
1515
@property()
1616
public test?: TestModel;
1717

packages/elements/src/components/file/file.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import { prismjs, tailwind } from '../../style';
1010
import { StateFilter } from '../state-filter/state-filter.component';
1111
import style from './file.scss';
1212
import { renderDots, renderLine } from './util';
13-
import { RealtimeElement } from '../realtime-element';
13+
import { RealTimeElement } from '../real-time-element';
1414

1515
const diffOldClass = 'diff-old';
1616
const diffNewClass = 'diff-new';
1717
@customElement('mte-file')
18-
export class FileComponent extends RealtimeElement {
18+
export class FileComponent extends RealTimeElement {
1919
static styles = [prismjs, tailwind, unsafeCSS(style)];
2020

2121
@state()

packages/elements/src/components/metrics-table/metrics-table.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Thresholds } from 'mutation-testing-report-schema/api';
77
import { toAbsoluteUrl } from '../../lib/html-helpers';
88
import { tailwind } from '../../style';
99
import { renderEmoji } from '../drawer-mutant/util';
10-
import { RealtimeElement } from '../realtime-element';
10+
import { RealTimeElement } from '../real-time-element';
1111

1212
export type TableWidth = 'normal' | 'large';
1313

@@ -25,7 +25,7 @@ export interface Column<TMetric> {
2525
}
2626

2727
@customElement('mte-metrics-table')
28-
export class MutationTestReportTestMetricsTable<TFile, TMetric> extends RealtimeElement {
28+
export class MutationTestReportTestMetricsTable<TFile, TMetric> extends RealTimeElement {
2929
@property()
3030
public model?: MetricsResult<TFile, TMetric>;
3131

packages/elements/src/components/mutant-view/mutant-view.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { tailwind } from '../../style';
77
import { DrawerMode } from '../drawer/drawer.component';
88
import { Column } from '../metrics-table/metrics-table.component';
99
import style from './mutant-view.scss';
10-
import { RealtimeElement } from '../realtime-element';
10+
import { RealTimeElement } from '../real-time-element';
1111

1212
@customElement('mte-mutant-view')
13-
export class MutationTestReportMutantViewComponent extends RealtimeElement {
13+
export class MutationTestReportMutantViewComponent extends RealTimeElement {
1414
@property()
1515
public drawerMode: DrawerMode = 'closed';
1616

packages/elements/src/components/realtime-element.ts renamed to packages/elements/src/components/real-time-element.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { LitElement } from 'lit';
22
import { Subscription } from 'rxjs';
33
import { mutantChanges } from '../lib/mutant-changes';
44

5-
export abstract class RealtimeElement extends LitElement {
5+
export abstract class RealTimeElement extends LitElement {
66
public shouldReactivate(): boolean {
77
return true;
88
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { LitElement, html } from 'lit';
2+
import { customElement, property, state } from 'lit/decorators.js';
3+
import { tailwind } from '../../style';
4+
5+
type ProgressType = 'detected' | 'undetected' | 'ignored + invalid' | 'pending';
6+
type ProgressMetric = { type: ProgressType; amount: number; tooltip: string };
7+
8+
@customElement('mte-result-status-bar')
9+
export class ResultStatusBar extends LitElement {
10+
public static styles = [tailwind];
11+
12+
@property({ attribute: false })
13+
public detected = 0;
14+
15+
@property({ attribute: false })
16+
public undetected = 0;
17+
18+
@property({ attribute: false })
19+
public invalid = 0;
20+
21+
@property({ attribute: false })
22+
public ignored = 0;
23+
24+
@property({ attribute: false })
25+
public pending = 0;
26+
27+
@property({ attribute: false })
28+
public total = 0;
29+
30+
@state()
31+
private shouldBeSmall = false;
32+
33+
#observer: IntersectionObserver | undefined;
34+
35+
public constructor() {
36+
super();
37+
}
38+
39+
public connectedCallback(): void {
40+
super.connectedCallback();
41+
42+
// This code is responsible for making the small progress-bar show up.
43+
// Once this element (the standard progress-bar) is no longer intersecting (visible) the viewable window,
44+
// the smaller progress-bar will show up at the top if the window.
45+
// If this element is visible, the smaller progress-bar will fade out and it will no longer be visible.
46+
this.#observer = new window.IntersectionObserver(([entry]) => {
47+
if (entry.isIntersecting) {
48+
this.shouldBeSmall = false;
49+
} else {
50+
this.shouldBeSmall = true;
51+
}
52+
53+
this.requestUpdate();
54+
});
55+
this.#observer.observe(this);
56+
}
57+
58+
public disconnectedCallback(): void {
59+
super.disconnectedCallback();
60+
61+
this.#observer?.disconnect();
62+
}
63+
64+
public render() {
65+
return html`
66+
${this.#renderSmallParts()}
67+
<div class="my-4 rounded-md border border-gray-200 bg-white transition-all">
68+
<div class="relative">
69+
<div class="parts flex h-8 w-full overflow-hidden rounded bg-gray-200">${this.#renderParts()}</div>
70+
</div>
71+
</div>
72+
`;
73+
}
74+
75+
#renderSmallParts() {
76+
return html`<div
77+
class="${this.shouldBeSmall ? 'opacity-1' : 'opacity-0'} pointer-events-none fixed left-0 top-0 z-20 flex w-full justify-center transition-all"
78+
>
79+
<div class="container w-full bg-white py-2">
80+
<div class="flex h-2 overflow-hidden rounded bg-gray-200">${this.#getMetrics().map((metric) => this.#renderSmallPart(metric))}</div>
81+
</div>
82+
</div>`;
83+
}
84+
85+
#renderSmallPart(metric: ProgressMetric) {
86+
return html`<div
87+
class="${this.#colorFromMetric(metric.type)} z-20 h-2 transition-all"
88+
style="width: ${this.#calculatePercentage(metric.amount)}%"
89+
></div>`;
90+
}
91+
92+
#renderParts() {
93+
return html`${this.#getMetrics().map((metric) => this.#renderPart(metric))}`;
94+
}
95+
96+
#renderPart(metric: ProgressMetric) {
97+
return html`<div
98+
title="${metric.tooltip}"
99+
style="width: ${this.#calculatePercentage(metric.amount)}%"
100+
class="${this.#colorFromMetric(metric.type)} ${metric.amount === 0
101+
? 'opacity-0'
102+
: ''} relative flex h-8 items-center overflow-hidden transition-all"
103+
>
104+
<span class="ms-3 font-bold text-gray-800">${metric.amount}</span>
105+
</div>`;
106+
}
107+
108+
#getMetrics(): Array<ProgressMetric> {
109+
return [
110+
{ type: 'detected', amount: this.detected, tooltip: 'killed + timeout' },
111+
{ type: 'undetected', amount: this.undetected, tooltip: 'survived + no coverage' },
112+
{ type: 'ignored + invalid', amount: this.ignored + this.invalid, tooltip: 'ignored + runtime error + compile error' },
113+
{ type: 'pending', amount: this.pending, tooltip: 'pending' },
114+
];
115+
}
116+
117+
#colorFromMetric(metric: ProgressType) {
118+
switch (metric) {
119+
case 'detected':
120+
return 'bg-green-600';
121+
case 'undetected':
122+
return 'bg-red-600';
123+
case 'ignored + invalid':
124+
return 'bg-yellow-600';
125+
default:
126+
return 'bg-gray-200';
127+
}
128+
}
129+
130+
#calculatePercentage(metric: number) {
131+
return this.total !== 0 ? (100 * metric) / this.total : 0;
132+
}
133+
}

packages/elements/src/components/state-filter/state-filter.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { createCustomEvent } from '../../lib/custom-events';
55
import { renderIf } from '../../lib/html-helpers';
66
import { tailwind } from '../../style';
77
import style from './state-filter.scss';
8-
import { RealtimeElement } from '../realtime-element';
8+
import { RealTimeElement } from '../real-time-element';
99

1010
export interface StateFilter<TStatus> {
1111
status: TStatus;
@@ -16,7 +16,7 @@ export interface StateFilter<TStatus> {
1616
}
1717

1818
@customElement('mte-state-filter')
19-
export class FileStateFilterComponent<TStatus extends string> extends RealtimeElement {
19+
export class FileStateFilterComponent<TStatus extends string> extends RealTimeElement {
2020
static styles = [tailwind, unsafeCSS(style)];
2121

2222
@property({ type: Array })
@@ -53,7 +53,7 @@ export class FileStateFilterComponent<TStatus extends string> extends RealtimeEl
5353

5454
public render() {
5555
return html`
56-
<div class="sticky top-offset z-10 my-1 flex flex-row bg-white py-4">
56+
<div class="sticky top-offset z-10 flex flex-row bg-white py-6">
5757
<div class="mr-3">
5858
<button title="Previous" @click=${this.previous} type="button" class="step-button">
5959
<svg aria-hidden="true" class="h-4 w-4 rotate-180" fill="white" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">

packages/elements/src/components/test-file/test-file.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import { prismjs, tailwind } from '../../style';
1212
import '../../style/prism-plugins';
1313
import { renderDots, renderLine } from '../file/util';
1414
import { StateFilter } from '../state-filter/state-filter.component';
15-
import { RealtimeElement } from '../realtime-element';
15+
import { RealTimeElement } from '../real-time-element';
1616

1717
@customElement('mte-test-file')
18-
export class TestFileComponent extends RealtimeElement {
18+
export class TestFileComponent extends RealTimeElement {
1919
public static styles = [prismjs, tailwind, unsafeCSS(style)];
2020

2121
@property()

packages/elements/src/components/test-view/test-view.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { tailwind } from '../../style';
66
import { DrawerMode } from '../drawer/drawer.component';
77
import { Column } from '../metrics-table/metrics-table.component';
88
import style from './test-view.scss';
9-
import { RealtimeElement } from '../realtime-element';
9+
import { RealTimeElement } from '../real-time-element';
1010

1111
@customElement('mte-test-view')
12-
export class MutationTestReportTestViewComponent extends RealtimeElement {
12+
export class MutationTestReportTestViewComponent extends RealTimeElement {
1313
@property()
1414
public drawerMode: DrawerMode = 'closed';
1515

packages/elements/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ import './components/test-file/test-file.component';
1212
import './components/drawer-test/drawer-test.component';
1313
import './components/file-icon/file-icon.component';
1414
import './components/tooltip/tooltip.component';
15+
import './components/result-status-bar/result-status-bar';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { PageObject } from './PageObject.po';
2+
3+
export class RealTimeProgressBar extends PageObject {
4+
public async smallProgressBarVisible() {
5+
const smallProgressBar = await this.$('div.pointer-events-none');
6+
return (await smallProgressBar.getCssValue('opacity')) === '1';
7+
}
8+
9+
public async progressBarVisible() {
10+
try {
11+
await this.$('div.my-4');
12+
return true;
13+
} catch {
14+
return false;
15+
}
16+
}
17+
18+
public async progressBarWidth() {
19+
return await this.$('.parts > div').getCssValue('width');
20+
}
21+
22+
public async killedCount() {
23+
return await this.$('.parts > div > span').getText();
24+
}
25+
}

0 commit comments

Comments
 (0)