From 4966721687f5848ce8cbb876d69a356ad55efcc5 Mon Sep 17 00:00:00 2001 From: Robson Oliveira dos Santos Date: Mon, 2 Dec 2024 22:04:46 +0930 Subject: [PATCH] refactor(spie-ui): create serial port service --- apps/spie-ui-e2e/src/e2e/updater.ts | 201 ++++++++++++ apps/spie-ui/src/app/app.component.ts | 4 +- .../connection-advanced-modal.component.ts | 8 +- .../home/connection/connection.component.html | 5 +- .../home/connection/connection.component.ts | 53 +++- .../src/app/pages/home/home.component.html | 20 +- .../src/app/pages/home/home.component.ts | 299 +----------------- .../send-advanced-modal.component.ts | 8 +- .../app/pages/home/send/send.component.html | 2 +- .../src/app/pages/home/send/send.component.ts | 9 +- .../terminal-advanced-modal.component.ts | 20 +- .../home/terminal/terminal.component.html | 5 +- .../pages/home/terminal/terminal.component.ts | 33 +- .../update-modal/update-modal.component.ts | 116 ++++++- .../src/app/services/serial-port.service.ts | 133 ++++++++ 15 files changed, 544 insertions(+), 372 deletions(-) create mode 100644 apps/spie-ui-e2e/src/e2e/updater.ts create mode 100644 apps/spie-ui/src/app/services/serial-port.service.ts diff --git a/apps/spie-ui-e2e/src/e2e/updater.ts b/apps/spie-ui-e2e/src/e2e/updater.ts new file mode 100644 index 0000000..e91c231 --- /dev/null +++ b/apps/spie-ui-e2e/src/e2e/updater.ts @@ -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' + ); + }); + }); +}); diff --git a/apps/spie-ui/src/app/app.component.ts b/apps/spie-ui/src/app/app.component.ts index 5fced85..14a6860 100644 --- a/apps/spie-ui/src/app/app.component.ts +++ b/apps/spie-ui/src/app/app.component.ts @@ -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 }); diff --git a/apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.ts b/apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.ts index 89a32c0..7ec5e93 100644 --- a/apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.ts +++ b/apps/spie-ui/src/app/pages/home/connection/connection-advanced-modal/connection-advanced-modal.component.ts @@ -1,4 +1,4 @@ -import { Component, input, model, viewChild } from '@angular/core'; +import { Component, inject, input, viewChild } from '@angular/core'; import { IonButton, IonButtons, @@ -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', @@ -42,8 +42,10 @@ import { ], }) export class ConnectionAdvancedComponent { + private readonly serialPortService = inject(SerialPortService); + reconnectSubject = input.required>(); - openOptions = model.required(); + openOptions = this.serialPortService.openOptions; connectionAdvancedModal = viewChild.required( 'connectionAdvancedModal' diff --git a/apps/spie-ui/src/app/pages/home/connection/connection.component.html b/apps/spie-ui/src/app/pages/home/connection/connection.component.html index 5595e27..81dd7ee 100644 --- a/apps/spie-ui/src/app/pages/home/connection/connection.component.html +++ b/apps/spie-ui/src/app/pages/home/connection/connection.component.html @@ -69,8 +69,5 @@ - + diff --git a/apps/spie-ui/src/app/pages/home/connection/connection.component.ts b/apps/spie-ui/src/app/pages/home/connection/connection.component.ts index 5c914ed..f1ee258 100644 --- a/apps/spie-ui/src/app/pages/home/connection/connection.component.ts +++ b/apps/spie-ui/src/app/pages/home/connection/connection.component.ts @@ -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, @@ -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({ @@ -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>(); - isOpen = input.required(); - openOptions = model.required(); + isOpen = this.serialPortService.isOpen; + openOptions = this.serialPortService.openOptions; + reconnectSubject = new Subject(); private connectionAdvancedComponent = viewChild.required( ConnectionAdvancedComponent @@ -111,7 +130,7 @@ export class ConnectionComponent { baudRate: parseInt(selectedOption, 10), })); - this.reconnectSubject().next(); + this.reconnectSubject.next(); } async onClickDisconnect(): Promise { diff --git a/apps/spie-ui/src/app/pages/home/home.component.html b/apps/spie-ui/src/app/pages/home/home.component.html index 6493f6b..4fa7c2f 100644 --- a/apps/spie-ui/src/app/pages/home/home.component.html +++ b/apps/spie-ui/src/app/pages/home/home.component.html @@ -5,22 +5,10 @@ - - - - - - - - + + + +