Skip to content

Commit

Permalink
refactor(spie-ui): create serial port service
Browse files Browse the repository at this point in the history
  • Loading branch information
robsonos committed Dec 2, 2024
1 parent 01c0d34 commit 4966721
Show file tree
Hide file tree
Showing 15 changed files with 544 additions and 372 deletions.
201 changes: 201 additions & 0 deletions apps/spie-ui-e2e/src/e2e/updater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { type SerialPortEvent } from '@spie/types';

import { mockElectronAPI } from '../fixtures/mocks/electron-api.mock';

describe('Send component', () => {
const mockSerialPortList = [
{ path: '/dev/ttyUSB0', manufacturer: 'Manufacturer1' },
{ path: '/dev/ttyUSB1', manufacturer: 'Manufacturer2' },
];

let onEventTrigger: ((event: SerialPortEvent) => void) | null;

beforeEach(() => {
cy.visit('/');

cy.on('window:before:load', (win) => {
const listeners: Array<(serialPortEvent: SerialPortEvent) => void> = [];

win.electron = mockElectronAPI();
win.electron.serialPort.list = cy.stub().resolves(mockSerialPortList);

win.electron.serialPort.onEvent = cy
.stub()
.callsFake((callback: (serialPortEvent: SerialPortEvent) => void) => {
listeners.push(callback);

onEventTrigger = (serialPortEvent) => {
listeners.forEach((listener) => listener(serialPortEvent));
};

return () => {
const index = listeners.indexOf(callback);
if (index !== -1) {
listeners.splice(index, 1);
}
};
});
});
});

it('should enable/disable send based on serial port status', () => {
const data = 'test test test test test test test test test test';

cy.wrap(null).then(() => {
if (onEventTrigger) {
onEventTrigger({ event: 'open' });
}
});

cy.get('app-send ion-input input').invoke('val', data).trigger('input');

cy.get('app-send ion-button')
.contains('Send')
.should('not.have.class', 'button-disabled');

cy.wrap(null).then(() => {
if (onEventTrigger) {
onEventTrigger({ event: 'close' });
}
});

cy.get('app-send ion-button')
.contains('Send')
.should('have.class', 'button-disabled');
});

it('should clear input after pressing clear input button', () => {
const data = 'test test test test test test test test test test';

cy.wrap(null).then(() => {
if (onEventTrigger) {
onEventTrigger({ event: 'open' });
}
});

cy.get('app-send ion-input input').invoke('val', data).trigger('input');
cy.get('app-send ion-input button').click();

cy.get('app-send ion-input input').should('have.value', '');
});

it('should send input with default options', () => {
const data = 'test test test test test test test test test test';
const formattedData = `${data}\n`;

cy.wrap(null).then(() => {
if (onEventTrigger) {
onEventTrigger({ event: 'open' });
}
});

cy.get('app-send ion-input input').invoke('val', data).trigger('input');

cy.get('app-send ion-button').contains('Send').click();

cy.window().then((win) => {
cy.wrap(win.electron.serialPort.write).should(
'have.been.calledOnceWithExactly',
formattedData,
'ascii'
);
});
});

it('should open and close the advanced modal', () => {
cy.get('app-send ion-button ion-icon').parent().click();
cy.get('ion-modal').should('be.visible');
cy.get('ion-modal ion-toolbar ion-button').click();
cy.get('ion-modal').should('not.be.visible');
});

it('should clear input after changing encoding', () => {
const data = 'test test test test test test test test test test';
cy.get('app-send ion-input input').invoke('val', data).trigger('input');

cy.get('app-send ion-button ion-icon').parent().click();
cy.getAdvancedModalSelectElement(
'send-advanced-modal',
'Encoding'
).selectOption('Hex');
cy.get('ion-modal ion-toolbar ion-button').click();

cy.get('app-send ion-input input').should('have.value', '');
});

it('should format hex input', () => {
const data = 'test test test test test test test test test test';
const expectedHexData = 'EE EE EE EE EE';

cy.get('app-send ion-button ion-icon').parent().click();
cy.getAdvancedModalSelectElement(
'send-advanced-modal',
'Encoding'
).selectOption('Hex');
cy.get('ion-modal ion-toolbar ion-button').click();

cy.get('app-send ion-input input').invoke('val', data).trigger('input');

cy.get('app-send ion-input input').should('have.value', expectedHexData);
});

it('should send input with hex encoding', () => {
const data = 'test test test test test test test test test test\n\n\n';
const formattedData = 'EEEEEEEEEE';

cy.get('app-send ion-button ion-icon').parent().click();
cy.getAdvancedModalSelectElement(
'send-advanced-modal',
'Encoding'
).selectOption('Hex');
cy.get('ion-modal ion-toolbar ion-button').click();

cy.wrap(null).then(() => {
if (onEventTrigger) {
onEventTrigger({ event: 'open' });
}
});

cy.get('app-send ion-input input').invoke('val', data).trigger('input');

cy.get('app-send ion-button').contains('Send').click();

cy.window().then((win) => {
cy.wrap(win.electron.serialPort.write).should(
'have.been.calledOnceWithExactly',
formattedData,
'hex'
);
});
});

it('should send input with advanced delimiter', () => {
const data = 'test test test test test test test test test test';
const formattedData = `${data}\r\n`;

cy.get('app-send ion-button ion-icon').parent().click();
cy.getAdvancedModalSelectElement(
'send-advanced-modal',
'Delimiter'
).selectOption('CRLF (\\r\\n)');
cy.get('ion-modal ion-toolbar ion-button').click();

cy.wrap(null).then(() => {
if (onEventTrigger) {
onEventTrigger({ event: 'open' });
}
});

cy.get('app-send ion-input input').invoke('val', data).trigger('input');

cy.get('app-send ion-button').contains('Send').click();

cy.window().then((win) => {
cy.wrap(win.electron.serialPort.write).should(
'have.been.calledOnceWithExactly',
formattedData,
'ascii'
);
});
});
});
4 changes: 2 additions & 2 deletions apps/spie-ui/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
})
export class AppComponent {
constructor() {
addIcons({ settingsOutline });
addIcons({ documentOutline });
addIcons({ cloudUploadOutline });
addIcons({ documentOutline });
addIcons({ settingsOutline });
addIcons({ speedometerOutline });
addIcons({ statsChartOutline });
addIcons({ timeOutline });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, input, model, viewChild } from '@angular/core';
import { Component, inject, input, viewChild } from '@angular/core';
import {
IonButton,
IonButtons,
Expand All @@ -13,13 +13,13 @@ import {
IonTitle,
IonToolbar,
} from '@ionic/angular/standalone';
import { type OpenOptions } from '@serialport/bindings-interface';
import { type Subject } from 'rxjs';

import {
type CheckboxCustomEvent,
type SelectCustomEvent,
} from '../../../../interfaces/ionic.interface';
import { SerialPortService } from '../../../../services/serial-port.service';

@Component({
selector: 'app-connection-advanced-modal',
Expand All @@ -42,8 +42,10 @@ import {
],
})
export class ConnectionAdvancedComponent {
private readonly serialPortService = inject(SerialPortService);

reconnectSubject = input.required<Subject<void>>();
openOptions = model.required<OpenOptions>();
openOptions = this.serialPortService.openOptions;

connectionAdvancedModal = viewChild.required<IonModal>(
'connectionAdvancedModal'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,5 @@
</ion-row>
</ion-grid>

<app-connection-advanced-modal
[(openOptions)]="openOptions"
[reconnectSubject]="reconnectSubject()"
/>
<app-connection-advanced-modal [reconnectSubject]="reconnectSubject" />
</ion-card>
53 changes: 36 additions & 17 deletions apps/spie-ui/src/app/pages/home/connection/connection.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import {
Component,
inject,
input,
model,
signal,
viewChild,
} from '@angular/core';
import { Component, inject, signal, viewChild } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
IonButton,
IonCard,
Expand All @@ -20,15 +14,13 @@ import {
IonText,
LoadingController,
} from '@ionic/angular/standalone';
import {
type OpenOptions,
type PortInfo,
} from '@serialport/bindings-interface';
import { type Subject } from 'rxjs';
import { type PortInfo } from '@serialport/bindings-interface';
import { Subject, tap } from 'rxjs';

import { ConnectionAdvancedComponent } from './connection-advanced-modal/connection-advanced-modal.component';
import { type SelectCustomEvent } from '../../../interfaces/ionic.interface';
import { ElectronService } from '../../../services/electron.service';
import { SerialPortService } from '../../../services/serial-port.service';
import { ToasterService } from '../../../services/toaster.service';

@Component({
Expand All @@ -55,10 +47,37 @@ export class ConnectionComponent {
private readonly loadingController = inject(LoadingController);
private readonly toasterService = inject(ToasterService);
private readonly electronService = inject(ElectronService);
private readonly serialPortService = inject(SerialPortService);

constructor() {
this.reconnectSubject
.pipe(
takeUntilDestroyed(),

tap(async () => {
if (this.isOpen()) {
const loading = await this.loadingController.create();
await loading.present();
try {
await this.electronService.serialPort.close();
await this.electronService.serialPort.open(this.openOptions());
this.serialPortService.clearDataSubject.next({
event: 'data',
data: '',
});
} catch (error) {
await this.toasterService.presentErrorToast(error);
}
await loading.dismiss();
}
})
)
.subscribe();
}

reconnectSubject = input.required<Subject<void>>();
isOpen = input.required<boolean>();
openOptions = model.required<OpenOptions>();
isOpen = this.serialPortService.isOpen;
openOptions = this.serialPortService.openOptions;
reconnectSubject = new Subject<void>();

private connectionAdvancedComponent = viewChild.required(
ConnectionAdvancedComponent
Expand Down Expand Up @@ -111,7 +130,7 @@ export class ConnectionComponent {
baudRate: parseInt(selectedOption, 10),
}));

this.reconnectSubject().next();
this.reconnectSubject.next();
}

async onClickDisconnect(): Promise<void> {
Expand Down
20 changes: 4 additions & 16 deletions apps/spie-ui/src/app/pages/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,10 @@
</ion-header>

<ion-content>
<app-connection
[(openOptions)]="openOptions"
[isOpen]="isOpen()"
[reconnectSubject]="reconnectSubject"
/>

<app-terminal
[(terminalOptions)]="terminalOptions"
[clearTerminalSubject]="clearTerminalSubject"
[data]="data()"
/>

<app-send [(sendOptions)]="sendOptions" [isOpen]="isOpen()" />

<app-update-modal [progressInfo]="progressInfo()" />

<app-connection />
<app-terminal />
<app-send />
<app-update-modal />
<!-- For debug purposes -->
<!-- <pre>{{ openOptions() | json }}</pre>
<pre>{{ terminalOptions() | json }}</pre>
Expand Down
Loading

0 comments on commit 4966721

Please sign in to comment.