Skip to content

Commit ac716ee

Browse files
authored
feat: add progress reporting functionality with EventSource integration (#1336)
1 parent ebde7d3 commit ac716ee

35 files changed

+11814
-35714
lines changed

.github/workflows/test-js.yml

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Test:JS
2+
3+
on:
4+
pull_request:
5+
6+
jobs:
7+
test:
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v3
13+
14+
- name: Setup Node.js
15+
uses: actions/setup-node@v2
16+
with:
17+
node-version: '16'
18+
19+
- name: Inject access token in .npmrc
20+
run: |
21+
echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> ~/.npmrc
22+
23+
- name: Cache Node.js modules
24+
uses: actions/cache@v3
25+
with:
26+
path: ~/.npm
27+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
28+
restore-keys: |
29+
${{ runner.os }}-node-
30+
31+
- name: Install dependencies
32+
run: npm install
33+
34+
- name: Run tests
35+
run: npm test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import { IProgressBar } from "./IProgressBar";
6+
7+
export default class EventSourceHandlerWithProgressBar {
8+
private source: EventSource | null = null;
9+
10+
constructor(private target: HTMLElement, private url: string, private progressBar: IProgressBar) {
11+
}
12+
13+
public start(): void {
14+
this.source = new EventSource(this.url);
15+
this.disableAllTriggersWithSameUrl();
16+
this.progressBar.show();
17+
this.addEventListeners(this.source);
18+
}
19+
20+
private disableAllTriggersWithSameUrl(): void {
21+
document.querySelectorAll(`[data-js-progress-url="${this.url}"]`).forEach((element) => {
22+
element.setAttribute('disabled', 'disabled');
23+
});
24+
}
25+
26+
27+
private addEventListeners(source: EventSource): void {
28+
source.addEventListener('message', this.updateLabel.bind(this));
29+
source.addEventListener('progress', this.updateProgress.bind(this));
30+
source.addEventListener('finish', this.finish.bind(this));
31+
}
32+
33+
private removeEventListeners(source: EventSource): void {
34+
source.removeEventListener('message', this.updateLabel.bind(this));
35+
source.removeEventListener('progress', this.updateProgress.bind(this));
36+
source.removeEventListener('finish', this.finish.bind(this));
37+
}
38+
39+
private updateLabel(event: MessageEvent) {
40+
this.progressBar.update({ label: event.data, value: null });
41+
}
42+
43+
private updateProgress(event: MessageEvent) {
44+
this.progressBar.update({ label: null, value: event.data });
45+
}
46+
47+
private finish(event: MessageEvent) {
48+
this.progressBar.update({ label: event.data, value: 100 });
49+
this.source!.close();
50+
this.removeEventListeners(this.source!);
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface EventSourceProgressHandler {
2+
start(): void;
3+
stop(): void;
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import EventSourceTrigger from './EventSourceTrigger';
6+
import EventSourceHandlerWithProgressBar from './EventSourceHandlerWithProgressBar';
7+
import ProgressBar from './ProgressBar';
8+
import ProgressBarWithLabel from './UIComponents/ProgressBarWithLabel';
9+
10+
jest.mock('./EventSourceHandlerWithProgressBar');
11+
jest.mock('./ProgressBar');
12+
jest.mock('./UIComponents/ProgressBarWithLabel');
13+
14+
describe('EventSourceTrigger', () => {
15+
let triggerElement: HTMLElement;
16+
let eventSourceUrl: string;
17+
let eventSourceTrigger: EventSourceTrigger;
18+
19+
beforeEach(() => {
20+
triggerElement = document.createElement('button');
21+
eventSourceUrl = 'http://example.com/events';
22+
eventSourceTrigger = new EventSourceTrigger(triggerElement, eventSourceUrl);
23+
});
24+
25+
it('should initialize EventSourceHandlerWithProgressBar on instantiation', () => {
26+
expect(EventSourceHandlerWithProgressBar).toHaveBeenCalledWith(triggerElement, eventSourceUrl, expect.any(ProgressBar));
27+
});
28+
29+
it('should add click event listener to triggerElement', () => {
30+
const addEventListenerSpy = jest.spyOn(triggerElement, 'addEventListener');
31+
new EventSourceTrigger(triggerElement, eventSourceUrl);
32+
expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function));
33+
});
34+
35+
it('should prevent default action and start EventSourceHandlerWithProgressBar on click', () => {
36+
const event = new MouseEvent('click', { bubbles: true, cancelable: true });
37+
const preventDefaultSpy = jest.spyOn(event, 'preventDefault');
38+
const startSpy = jest.spyOn(eventSourceTrigger['eventSourceHandlerWithProgressBar'], 'start');
39+
40+
triggerElement.dispatchEvent(event);
41+
42+
expect(preventDefaultSpy).toHaveBeenCalled();
43+
expect(startSpy).toHaveBeenCalled();
44+
});
45+
46+
it('should create a ProgressBar instance in createProgressBar', () => {
47+
const progressBar = eventSourceTrigger['createProgressBar']();
48+
expect(progressBar).toBeInstanceOf(ProgressBar);
49+
});
50+
51+
it('should create a ProgressBarWithLabel element in createProgressBar', () => {
52+
const progressBarElement = document.createElement(ProgressBarWithLabel.customElementName);
53+
document.createElement = jest.fn().mockReturnValue(progressBarElement);
54+
55+
eventSourceTrigger['createProgressBar']();
56+
57+
expect(document.createElement).toHaveBeenCalledWith(ProgressBarWithLabel.customElementName);
58+
});
59+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import EventSourceHandlerWithProgressBar from "./EventSourceHandlerWithProgressBar";
2+
import { IProgressBar } from "./IProgressBar";
3+
import ProgressBar from "./ProgressBar";
4+
import ProgressBarWithLabel from "./UIComponents/ProgressBarWithLabel";
5+
6+
export default class EventSourceTrigger {
7+
8+
private eventSourceHandlerWithProgressBar: EventSourceHandlerWithProgressBar;
9+
10+
constructor(private triggerElement: HTMLElement, private eventSourceUrl: string) {
11+
this.eventSourceHandlerWithProgressBar = new EventSourceHandlerWithProgressBar(this.triggerElement, this.eventSourceUrl, this.createProgressBar());
12+
this.triggerElement.addEventListener('click', this.handleClick.bind(this));
13+
}
14+
15+
private handleClick(event: Event) {
16+
event.preventDefault();
17+
this.eventSourceHandlerWithProgressBar.start();
18+
}
19+
20+
private createProgressBar(): IProgressBar {
21+
const progressBarElement = document.createElement(ProgressBarWithLabel.customElementName) as ProgressBarWithLabel;
22+
const progressBar = new ProgressBar(progressBarElement, this.triggerElement);
23+
return progressBar;
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type ProgressBarUpdate = {
2+
label: string | null;
3+
value: number | null;
4+
}
5+
6+
export interface IProgressBar {
7+
update(event: ProgressBarUpdate): void;
8+
show(): void;
9+
hide(): void;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import ProgressBar from './ProgressBar';
6+
import ProgressBarWithLabel from './UIComponents/ProgressBarWithLabel';
7+
import { ProgressBarUpdate } from './IProgressBar';
8+
9+
describe('ProgressBar', () => {
10+
let progressBar: ProgressBar;
11+
let element: ProgressBarWithLabel;
12+
let insertAfterElement: HTMLElement;
13+
14+
beforeEach(() => {
15+
element = document.createElement('progress-bar-with-label') as ProgressBarWithLabel;
16+
insertAfterElement = document.createElement('div');
17+
document.body.appendChild(insertAfterElement);
18+
progressBar = new ProgressBar(element, insertAfterElement);
19+
});
20+
21+
afterEach(() => {
22+
document.body.removeChild(insertAfterElement);
23+
});
24+
25+
test('should update label and progress value', () => {
26+
const event: ProgressBarUpdate = { label: 'Loading', value: 50 };
27+
28+
progressBar.update(event);
29+
30+
expect(element.getAttribute('label')).toBe('Loading');
31+
expect(element.getAttribute('progress')).toBe('50');
32+
});
33+
34+
test('should show the progress bar', () => {
35+
progressBar.show();
36+
37+
expect(insertAfterElement.nextSibling).toBe(element);
38+
});
39+
40+
test('should hide the progress bar', () => {
41+
progressBar.show();
42+
progressBar.hide();
43+
44+
expect(document.body.contains(element)).toBe(false);
45+
});
46+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { IProgressBar, ProgressBarUpdate } from "./IProgressBar";
2+
import ProgressBarWithLabel from "./UIComponents/ProgressBarWithLabel";
3+
4+
export default class ProgressBar implements IProgressBar {
5+
6+
constructor(private element: ProgressBarWithLabel, private insertAfterElement: HTMLElement) {
7+
}
8+
9+
public update(event: ProgressBarUpdate): void {
10+
if (event.label !== null) {
11+
this.element.setAttribute('label', event.label);
12+
}
13+
14+
if (event.value !== null) {
15+
this.element.setAttribute('progress', event.value.toString());
16+
}
17+
}
18+
19+
public show(): void {
20+
this.insertAfterElement.insertAdjacentElement('afterend', this.element);
21+
}
22+
23+
public hide(): void {
24+
this.element.remove();
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
progress {
2+
border: none;
3+
border-radius: 3px;
4+
background-color: #f3f3f3;
5+
margin-right: 8px;
6+
min-width: 192px;
7+
}
8+
9+
::-webkit-progress-bar {
10+
background-color: rgba(0, 0, 0, 0.15);
11+
border-radius: 3px;
12+
border: none;
13+
}
14+
15+
::-webkit-progress-value {
16+
background-color: #2271b1;
17+
border-radius: 3px;
18+
transition: width 1s;
19+
}
20+
21+
label {
22+
font-style: italic;
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import "./ProgressBarWithLabel";
6+
import ProgressBarWithLabel from "./ProgressBarWithLabel";
7+
8+
describe("<progress-bar-with-label>", () => {
9+
10+
beforeAll(() => {
11+
customElements.define("progress-bar-with-label", ProgressBarWithLabel);
12+
});
13+
14+
it("contains a <progress> element'", () => {
15+
const progressBar = document.createElement("progress-bar-with-label");
16+
expect(progressBar.shadowRoot!.querySelector("progress")).toBeTruthy();
17+
});
18+
19+
it("contains a <label> element'", () => {
20+
const progressBar = document.createElement("progress-bar-with-label");
21+
expect(progressBar.shadowRoot!.querySelector("label")).toBeTruthy();
22+
});
23+
24+
it("initializes with progress set to 0", () => {
25+
const progressBar = document.createElement("progress-bar-with-label");
26+
expect(progressBar.shadowRoot!.querySelector("progress")!.getAttribute("value")).toBe("0");
27+
});
28+
29+
it("can update progress value", () => {
30+
const progressBar = document.createElement("progress-bar-with-label");
31+
progressBar.setAttribute("progress", "50");
32+
expect(progressBar.shadowRoot!.querySelector("progress")!.getAttribute("value")).toBe("50");
33+
});
34+
35+
it("initializes with label set to empty string", () => {
36+
const progressBar = document.createElement("progress-bar-with-label");
37+
expect(progressBar.shadowRoot!.querySelector("label")!.textContent).toBe("");
38+
});
39+
40+
it("can update label value", () => {
41+
const progressBar = document.createElement("progress-bar-with-label");
42+
progressBar.setAttribute("label", "Loading...");
43+
expect(progressBar.shadowRoot!.querySelector("label")!.textContent).toBe("Loading...");
44+
});
45+
46+
it("handles invalid progress value gracefully", () => {
47+
const progressBar = document.createElement("progress-bar-with-label");
48+
progressBar.setAttribute("progress", "invalid");
49+
expect(progressBar.shadowRoot!.querySelector("progress")!.getAttribute("value")).toBe("0");
50+
});
51+
});

0 commit comments

Comments
 (0)