diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 436dd8e86..1cbc88518 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -271,8 +271,8 @@ import { MonitorModel } from './monitor-model'; import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl'; import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager'; import { EditorManager } from './theia/editor/editor-manager'; -import { HostedPluginEvents } from './hosted-plugin-events'; -import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin'; +import { HostedPluginEvents } from './hosted/hosted-plugin-events'; +import { HostedPluginSupportImpl } from './theia/plugin-ext/hosted-plugin'; import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; import { Formatter, FormatterPath } from '../common/protocol/formatter'; import { Format } from './contributions/format'; @@ -361,6 +361,7 @@ import { TerminalFrontendContribution as TheiaTerminalFrontendContribution } fro import { SelectionService } from '@theia/core/lib/common/selection-service'; import { CommandService } from '@theia/core/lib/common/command'; import { CorePreferences } from '@theia/core/lib/browser/core-preferences'; +import { HostedPluginSupport } from './hosted/hosted-plugin-support'; // Hack to fix copy/cut/paste issue after electron version update in Theia. // https://github.com/eclipse-theia/theia/issues/12487 @@ -982,8 +983,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { }) .inSingletonScope(); - bind(HostedPluginSupport).toSelf().inSingletonScope(); - rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport); + bind(HostedPluginSupportImpl).toSelf().inSingletonScope(); + bind(HostedPluginSupport).toService(HostedPluginSupportImpl); + rebind(TheiaHostedPluginSupport).toService(HostedPluginSupportImpl); bind(HostedPluginEvents).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(HostedPluginEvents); diff --git a/arduino-ide-extension/src/browser/contributions/debug.ts b/arduino-ide-extension/src/browser/contributions/debug.ts index 5dec4655c..f354a0860 100644 --- a/arduino-ide-extension/src/browser/contributions/debug.ts +++ b/arduino-ide-extension/src/browser/contributions/debug.ts @@ -1,6 +1,6 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { Event, Emitter } from '@theia/core/lib/common/event'; -import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { HostedPluginSupport } from '../hosted/hosted-plugin-support'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { NotificationCenter } from '../notification-center'; import { diff --git a/arduino-ide-extension/src/browser/contributions/ino-language.ts b/arduino-ide-extension/src/browser/contributions/ino-language.ts index 096c27ed8..c560fa57b 100644 --- a/arduino-ide-extension/src/browser/contributions/ino-language.ts +++ b/arduino-ide-extension/src/browser/contributions/ino-language.ts @@ -15,7 +15,7 @@ import { } from '../../common/protocol'; import { CurrentSketch } from '../sketches-service-client-impl'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; -import { HostedPluginEvents } from '../hosted-plugin-events'; +import { HostedPluginEvents } from '../hosted/hosted-plugin-events'; import { NotificationCenter } from '../notification-center'; import { SketchContribution, URI } from './contribution'; import { BoardsDataStore } from '../boards/boards-data-store'; diff --git a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts index e83e46f51..a23d8aec8 100644 --- a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts +++ b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts @@ -1,7 +1,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable'; import URI from '@theia/core/lib/common/uri'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { HostedPluginSupport } from '../hosted/hosted-plugin-support'; import type { ArduinoState } from 'vscode-arduino-api'; import { BoardsService, @@ -21,7 +21,10 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { CurrentSketch } from '../sketches-service-client-impl'; import { SketchContribution } from './contribution'; -interface UpdateStateParams { +/** + * (non-API) exported for tests + */ +export interface UpdateStateParams { readonly key: keyof T; readonly value: T[keyof T]; } diff --git a/arduino-ide-extension/src/browser/hosted-plugin-events.ts b/arduino-ide-extension/src/browser/hosted/hosted-plugin-events.ts similarity index 97% rename from arduino-ide-extension/src/browser/hosted-plugin-events.ts rename to arduino-ide-extension/src/browser/hosted/hosted-plugin-events.ts index ac7b1fe0d..35dbdbe4c 100644 --- a/arduino-ide-extension/src/browser/hosted-plugin-events.ts +++ b/arduino-ide-extension/src/browser/hosted/hosted-plugin-events.ts @@ -1,7 +1,7 @@ import { DisposableCollection, Emitter, Event } from '@theia/core'; import { FrontendApplicationContribution } from '@theia/core/lib/browser'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin'; +import { HostedPluginSupport } from './hosted-plugin-support'; /** * Frontend contribution to watch VS Code extension start/stop events from Theia. diff --git a/arduino-ide-extension/src/browser/hosted/hosted-plugin-support.ts b/arduino-ide-extension/src/browser/hosted/hosted-plugin-support.ts new file mode 100644 index 000000000..b0d11c86a --- /dev/null +++ b/arduino-ide-extension/src/browser/hosted/hosted-plugin-support.ts @@ -0,0 +1,14 @@ +import type { Event } from '@theia/core/lib/common/event'; + +/* +This implementation hides the default HostedPluginSupport implementation from Theia to be able to test it. +Otherwise, the default implementation fails at require time due to the `import.meta` in the Theia plugin worker code. +https://github.com/eclipse-theia/theia/blob/964f69ca3b3a5fb87ffa0177fb300b74ba0ca39f/packages/plugin-ext/src/hosted/browser/plugin-worker.ts#L30-L32 +*/ + +export const HostedPluginSupport = Symbol('HostedPluginSupport'); +export interface HostedPluginSupport { + readonly didStart: Promise; + readonly onDidLoad: Event; + readonly onDidCloseConnection: Event; +} diff --git a/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts b/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts index 40d703423..2604c3caf 100644 --- a/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts +++ b/arduino-ide-extension/src/browser/theia/monaco/monaco-theming-service.ts @@ -20,7 +20,7 @@ import { } from '@theia/monaco/lib/browser/monaco-theming-service'; import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry'; import type { ThemeMix } from '@theia/monaco/lib/browser/textmate/monaco-theme-types'; -import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { HostedPluginSupport } from '../../hosted/hosted-plugin-support'; import { ArduinoThemes, compatibleBuiltInTheme } from '../core/theming'; import { WindowServiceExt } from '../core/window-service-ext'; diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts index 8491357a4..8edf46383 100644 --- a/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts +++ b/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts @@ -5,9 +5,13 @@ import { PluginContributions, HostedPluginSupport as TheiaHostedPluginSupport, } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { HostedPluginSupport } from '../../hosted/hosted-plugin-support'; @injectable() -export class HostedPluginSupport extends TheiaHostedPluginSupport { +export class HostedPluginSupportImpl + extends TheiaHostedPluginSupport + implements HostedPluginSupport +{ private readonly onDidLoadEmitter = new Emitter(); private readonly onDidCloseConnectionEmitter = new Emitter(); diff --git a/arduino-ide-extension/src/test/browser/board-service-provider.test.ts b/arduino-ide-extension/src/test/browser/board-service-provider.test.ts index 039111474..54d3aa8ba 100644 --- a/arduino-ide-extension/src/test/browser/board-service-provider.test.ts +++ b/arduino-ide-extension/src/test/browser/board-service-provider.test.ts @@ -15,7 +15,6 @@ import { DisposableCollection, } from '@theia/core/lib/common/disposable'; import { MessageService } from '@theia/core/lib/common/message-service'; -import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import { Container, ContainerModule } from '@theia/core/shared/inversify'; import { expect } from 'chai'; import { BoardsDataStore } from '../../browser/boards/boards-data-store'; @@ -31,7 +30,6 @@ import { PortIdentifierChangeEvent, } from '../../common/protocol/boards-service'; import { NotificationServiceServer } from '../../common/protocol/notification-service'; -import { bindCommon, ConsoleLogger } from '../common/common-test-bindings'; import { detectedPort, esp32S3DevModule, @@ -41,6 +39,7 @@ import { uno, unoSerialPort, } from '../common/fixtures'; +import { bindBrowser } from './browser-test-bindings'; disableJSDOM(); @@ -392,7 +391,7 @@ describe('board-service-provider', () => { const container = new Container({ defaultScope: 'Singleton' }); container.load( new ContainerModule((bind, unbind, isBound, rebind) => { - bindCommon(bind); + bindBrowser(bind, unbind, isBound, rebind); bind(MessageService).toConstantValue({}); bind(BoardsService).toConstantValue({ getDetectedPorts() { @@ -414,11 +413,6 @@ describe('board-service-provider', () => { bind(WindowService).toConstantValue({}); bind(StorageService).toService(LocalStorageService); bind(BoardsServiceProvider).toSelf().inSingletonScope(); - // IDE2's test console logger does not support `Loggable` arg. - // Rebind logger to suppress `[Function (anonymous)]` messages in tests when the storage service is initialized without `window.localStorage`. - // https://github.com/eclipse-theia/theia/blob/04c8cf07843ea67402131132e033cdd54900c010/packages/core/src/browser/storage-service.ts#L60 - bind(MockLogger).toSelf().inSingletonScope(); - rebind(ConsoleLogger).toService(MockLogger); }) ); return container; diff --git a/arduino-ide-extension/src/test/browser/browser-test-bindings.ts b/arduino-ide-extension/src/test/browser/browser-test-bindings.ts index 9165765f7..7b950833b 100644 --- a/arduino-ide-extension/src/test/browser/browser-test-bindings.ts +++ b/arduino-ide-extension/src/test/browser/browser-test-bindings.ts @@ -1,8 +1,25 @@ +import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import { Container, ContainerModule } from '@theia/core/shared/inversify'; -import { bindCommon } from '../common/common-test-bindings'; +import { + Bind, + ConsoleLogger, + bindCommon, +} from '../common/common-test-bindings'; -export function createBaseContainer(): Container { +export function createBaseContainer(bind: Bind = bindBrowser): Container { const container = new Container({ defaultScope: 'Singleton' }); - container.load(new ContainerModule((bind) => bindCommon(bind))); + container.load(new ContainerModule(bind)); return container; } + +export const bindBrowser: Bind = function ( + ...args: Parameters +): ReturnType { + bindCommon(...args); + const [bind, , , rebind] = args; + // IDE2's test console logger does not support `Loggable` arg. + // Rebind logger to suppress `[Function (anonymous)]` messages in tests when the storage service is initialized without `window.localStorage`. + // https://github.com/eclipse-theia/theia/blob/04c8cf07843ea67402131132e033cdd54900c010/packages/core/src/browser/storage-service.ts#L60 + bind(MockLogger).toSelf().inSingletonScope(); + rebind(ConsoleLogger).toService(MockLogger); +}; diff --git a/arduino-ide-extension/src/test/browser/update-arduino-state.test.ts b/arduino-ide-extension/src/test/browser/update-arduino-state.test.ts new file mode 100644 index 000000000..c3ad66ce9 --- /dev/null +++ b/arduino-ide-extension/src/test/browser/update-arduino-state.test.ts @@ -0,0 +1,670 @@ +import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; +const disableJSDOM = enableJSDOM(); + +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +FrontendApplicationConfigProvider.set({}); + +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { LabelProvider } from '@theia/core/lib/browser/label-provider'; +import { OpenerService } from '@theia/core/lib/browser/opener-service'; +import { + LocalStorageService, + StorageService, +} from '@theia/core/lib/browser/storage-service'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { Emitter } from '@theia/core/lib/common/event'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { wait } from '@theia/core/lib/common/promise-util'; +import URI from '@theia/core/lib/common/uri'; +import { + Container, + ContainerModule, + injectable, +} from '@theia/core/shared/inversify'; +import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { expect } from 'chai'; +import type { + BoardDetails as ApiBoardDetails, + CompileSummary as ApiCompileSummary, + Port as ApiPort, +} from 'vscode-arduino-api'; +import { URI as CodeURI } from 'vscode-uri'; +import { ArduinoPreferences } from '../../browser/arduino-preferences'; +import { BoardsDataStore } from '../../browser/boards/boards-data-store'; +import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider'; +import { ConfigServiceClient } from '../../browser/config/config-service-client'; +import { CommandRegistry } from '../../browser/contributions/contribution'; +import { + UpdateArduinoState, + UpdateStateParams, +} from '../../browser/contributions/update-arduino-state'; +import { DialogService } from '../../browser/dialog-service'; +import { SettingsService } from '../../browser/dialogs/settings/settings'; +import { HostedPluginSupport } from '../../browser/hosted/hosted-plugin-support'; +import { NotificationCenter } from '../../browser/notification-center'; +import { + CurrentSketch, + SketchesServiceClientImpl, +} from '../../browser/sketches-service-client-impl'; +import { ApplicationConnectionStatusContribution } from '../../browser/theia/core/connection-status-service'; +import { OutputChannelManager } from '../../browser/theia/output/output-channel'; +import { WorkspaceService } from '../../browser/theia/workspace/workspace-service'; +import { MainMenuManager } from '../../common/main-menu-manager'; +import { + CompileSummary, + FileSystemExt, + SketchesService, +} from '../../common/protocol'; +import { + BoardDetails, + BoardsService, + Port, +} from '../../common/protocol/boards-service'; +import { NotificationServiceServer } from '../../common/protocol/notification-service'; +import { never } from '../utils'; +import { bindBrowser } from './browser-test-bindings'; + +disableJSDOM(); + +describe('update-arduino-state', function () { + this.slow(250); + + let toDisposeAfterEach: DisposableCollection; + let boardsServiceProvider: BoardsServiceProvider; + let notificationCenter: NotificationCenter; + let commandRegistry: CommandRegistry; + let updateArduinoState: UpdateArduinoState; + let stateUpdateParams: UpdateStateParams[]; + + let boardDetailsMocks: Record; + let dataStoreMocks: Record; + let currentSketchMock: CurrentSketch | undefined; + let sketchDirUriMock: URI | undefined; + let dataDirUriMock: URI | undefined; + let onCurrentSketchDidChangeEmitter: Emitter; + let onDataDirDidChangeEmitter: Emitter; + let onSketchDirDidChangeEmitter: Emitter; + let onDataStoreDidChangeEmitter: Emitter; + + beforeEach(async () => { + toDisposeAfterEach = new DisposableCollection(); + stateUpdateParams = []; + + // reset mocks + boardDetailsMocks = {}; + dataStoreMocks = {}; + currentSketchMock = undefined; + sketchDirUriMock = undefined; + dataDirUriMock = undefined; + onCurrentSketchDidChangeEmitter = new Emitter(); + onDataDirDidChangeEmitter = new Emitter(); + onSketchDirDidChangeEmitter = new Emitter(); + onDataStoreDidChangeEmitter = new Emitter(); + toDisposeAfterEach.pushAll([ + onCurrentSketchDidChangeEmitter, + onDataDirDidChangeEmitter, + onSketchDirDidChangeEmitter, + onDataStoreDidChangeEmitter, + ]); + + const container = createContainer(); + commandRegistry = container.get(CommandRegistry); + // This command is registered by vscode-arduino-api + commandRegistry.registerCommand( + { id: 'arduinoAPI.updateState' }, + { + execute: (params: UpdateStateParams) => stateUpdateParams.push(params), + } + ); + // This command is contributed by the vscode-arduino-tools VSIX + commandRegistry.registerCommand( + { id: 'arduino.languageserver.notifyBuildDidComplete' }, + { + execute: () => { + /* NOOP */ + }, + } + ); + container.get( + FrontendApplicationStateService + ).state = 'ready'; + boardsServiceProvider = container.get( + BoardsServiceProvider + ); + notificationCenter = container.get(NotificationCenter); + updateArduinoState = container.get(UpdateArduinoState); + toDisposeAfterEach.push( + Disposable.create(() => boardsServiceProvider.onStop()) + ); + boardsServiceProvider.onStart(); + await boardsServiceProvider.ready; + updateArduinoState.onStart(); + + await wait(50); + stateUpdateParams = []; + }); + + afterEach(() => { + toDisposeAfterEach.dispose(); + }); + + it('should automatically update the boards config (board+port) on ready', async () => { + const fqbn = 'a:b:c'; + const board = { fqbn, name: 'ABC' }; + const boardDetails = { + buildProperties: [], + configOptions: [], + debuggingSupported: false, + fqbn, + PID: '0', + VID: '0', + programmers: [], + requiredTools: [], + }; + boardDetailsMocks = { + 'a:b:c': boardDetails, + }; + const port = { address: 'COM1', protocol: 'serial' }; + boardsServiceProvider['_boardsConfig'] = { + selectedBoard: board, + selectedPort: port, + }; + boardsServiceProvider['_detectedPorts'] = { + [Port.keyOf(port)]: { + port: { + address: 'COM1', + addressLabel: 'COM1 Port', + protocol: 'serial', + protocolLabel: 'Serial', + }, + boards: [], + }, + }; + + updateArduinoState.onReady(); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => + param.key === 'fqbn' || + param.key === 'boardDetails' || + param.key === 'port' + ); + expect(params).to.be.deep.equal([ + { key: 'fqbn', value: 'a:b:c' }, + { + key: 'boardDetails', + value: { + buildProperties: {}, + configOptions: [], + fqbn: 'a:b:c', + programmers: [], + toolsDependencies: [], + } as ApiBoardDetails, + }, + { + key: 'port', + value: { + address: 'COM1', + protocol: 'serial', + protocolLabel: 'Serial', + hardwareId: '', + label: 'COM1 Port', + properties: {}, + } as ApiPort, + }, + ]); + }); + + it('should automatically update the sketch path on ready', async () => { + const uri = 'file:///path/to/my_sketch'; + currentSketchMock = { + name: 'my_sketch', + uri, + mainFileUri: 'file:///path/to/my_sketch/my_sketch.ino', + additionalFileUris: [], + otherSketchFileUris: [], + rootFolderFileUris: [], + }; + + updateArduinoState.onReady(); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'sketchPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'sketchPath', + value: CodeURI.parse(uri).fsPath, + }, + ]); + }); + + it("should automatically update the 'directories.data' path on ready", async () => { + const uri = 'file:///path/to/data/dir'; + dataDirUriMock = new URI(uri); + + stateUpdateParams = []; + updateArduinoState.onReady(); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'dataDirPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'dataDirPath', + value: CodeURI.parse(uri).fsPath, + }, + ]); + }); + + it("should automatically update the 'directories.user' path on ready", async () => { + const uri = 'file:///path/to/sketchbook'; + sketchDirUriMock = new URI(uri); + + updateArduinoState.onReady(); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'userDirPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'userDirPath', + value: CodeURI.parse(uri).fsPath, + }, + ]); + }); + + it('should update the boards config (board only) when did change', async () => { + const fqbn = 'a:b:c'; + const board = { fqbn, name: 'ABC' }; + const boardDetails = { + buildProperties: [], + configOptions: [], + debuggingSupported: false, + fqbn, + PID: '0', + VID: '0', + programmers: [], + requiredTools: [], + }; + boardDetailsMocks = { + 'a:b:c': boardDetails, + }; + boardsServiceProvider.updateConfig(board); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => + param.key === 'fqbn' || + param.key === 'boardDetails' || + param.key === 'port' + ); + expect(params).to.be.deep.equal([ + { key: 'fqbn', value: 'a:b:c' }, + { + key: 'boardDetails', + value: { + buildProperties: {}, + configOptions: [], + fqbn: 'a:b:c', + programmers: [], + toolsDependencies: [], + } as ApiBoardDetails, + }, + { key: 'port', value: undefined }, + ]); + }); + + it('should update the boards config (port only) when did change', async () => { + const port = { address: 'COM1', protocol: 'serial' }; + notificationCenter.notifyDetectedPortsDidChange({ + detectedPorts: { + [Port.keyOf(port)]: { + port: { + address: 'COM1', + addressLabel: 'COM1 Port', + protocol: 'serial', + protocolLabel: 'Serial', + }, + boards: [], + }, + }, + }); + boardsServiceProvider.updateConfig(port); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => + param.key === 'fqbn' || + param.key === 'boardDetails' || + param.key === 'port' + ); + expect(params).to.be.deep.equal([ + { key: 'fqbn', value: undefined }, + { key: 'boardDetails', value: undefined }, + { + key: 'port', + value: { + address: 'COM1', + protocol: 'serial', + protocolLabel: 'Serial', + hardwareId: '', + label: 'COM1 Port', + properties: {}, + } as ApiPort, + }, + ]); + }); + + it('should update the boards config (board+port) when did change', async () => { + const fqbn = 'a:b:c'; + const board = { fqbn, name: 'ABC' }; + const boardDetails = { + buildProperties: [], + configOptions: [], + debuggingSupported: false, + fqbn, + PID: '0', + VID: '0', + programmers: [], + requiredTools: [], + }; + boardDetailsMocks = { + 'a:b:c': boardDetails, + }; + const port = { address: 'COM1', protocol: 'serial' }; + boardsServiceProvider.updateConfig({ + selectedBoard: board, + selectedPort: port, + }); + notificationCenter.notifyDetectedPortsDidChange({ + detectedPorts: { + [Port.keyOf(port)]: { + port: { + address: 'COM1', + addressLabel: 'COM1 Port', + protocol: 'serial', + protocolLabel: 'Serial', + }, + boards: [], + }, + }, + }); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => + param.key === 'fqbn' || + param.key === 'boardDetails' || + param.key === 'port' + ); + expect(params).to.be.deep.equal([ + { key: 'fqbn', value: 'a:b:c' }, + { + key: 'boardDetails', + value: { + buildProperties: {}, + configOptions: [], + fqbn: 'a:b:c', + programmers: [], + toolsDependencies: [], + } as ApiBoardDetails, + }, + { + key: 'port', + value: { + address: 'COM1', + protocol: 'serial', + protocolLabel: 'Serial', + hardwareId: '', + label: 'COM1 Port', + properties: {}, + } as ApiPort, + }, + ]); + }); + + it('should update the compile summary after a verify', async () => { + const summary: CompileSummary = { + buildPath: '/path/to/build', + buildProperties: [], + executableSectionsSize: [], + usedLibraries: [], + boardPlatform: undefined, + buildPlatform: undefined, + buildOutputUri: 'file:///path/to/build', + }; + await commandRegistry.executeCommand( + 'arduino.languageserver.notifyBuildDidComplete', + summary + ); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'compileSummary' + ); + expect(params).to.be.deep.equal([ + { + key: 'compileSummary', + value: { + buildPath: '/path/to/build', + buildProperties: {}, + executableSectionsSize: [], + usedLibraries: [], + boardPlatform: undefined, + buildPlatform: undefined, + } as ApiCompileSummary, + }, + ]); + }); + + it('should update the current sketch when did change', async () => { + const uri = 'file:///path/to/my_sketch'; + const sketch = { + name: 'my_sketch', + uri, + mainFileUri: 'file:///path/to/my_sketch/my_sketch.ino', + additionalFileUris: [], + otherSketchFileUris: [], + rootFolderFileUris: [], + }; + onCurrentSketchDidChangeEmitter.fire(sketch); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'sketchPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'sketchPath', + value: CodeURI.parse(uri).fsPath, + }, + ]); + }); + + it("should update the 'directories.data' when did change", async () => { + const uri = new URI('file:///path/to/data/dir'); + onDataDirDidChangeEmitter.fire(uri); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'dataDirPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'dataDirPath', + value: CodeURI.parse(uri.toString()).fsPath, + }, + ]); + }); + + it("should update the 'directories.user' when did change", async () => { + const uri = new URI('file:///path/to/sketchbook'); + onSketchDirDidChangeEmitter.fire(uri); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => param.key === 'userDirPath' + ); + expect(params).to.be.deep.equal([ + { + key: 'userDirPath', + value: CodeURI.parse(uri.toString()).fsPath, + }, + ]); + }); + + it('should not update the board details when data store did change but the selected board does not match', async () => { + onDataStoreDidChangeEmitter.fire(['a:b:c']); + await wait(50); + + expect(stateUpdateParams).to.be.empty; + }); + + it('should update the board details when the data store did change and the selected board matches', async () => { + const fqbn = 'a:b:c'; + const board = { fqbn, name: 'ABC' }; + const boardDetails = { + buildProperties: [], + configOptions: [], + debuggingSupported: false, + fqbn, + PID: '0', + VID: '0', + programmers: [], + requiredTools: [], + }; + boardDetailsMocks = { + 'a:b:c': boardDetails, + }; + boardsServiceProvider['_boardsConfig'] = { + selectedBoard: board, + selectedPort: undefined, + }; + + onDataStoreDidChangeEmitter.fire(['a:b:c']); + await wait(50); + + const params = stateUpdateParams.filter( + (param) => + param.key === 'fqbn' || + param.key === 'boardDetails' || + param.key === 'port' + ); + expect(params).to.be.deep.equal([ + { + key: 'boardDetails', + value: { + buildProperties: {}, + configOptions: [], + fqbn: 'a:b:c', + programmers: [], + toolsDependencies: [], + } as ApiBoardDetails, + }, + ]); + }); + + function createContainer(): Container { + const container = new Container({ defaultScope: 'Singleton' }); + container.load( + new ContainerModule((bind, unbind, isBound, rebind) => { + bindBrowser(bind, unbind, isBound, rebind); + bind(MessageService).toConstantValue({}); + bind(BoardsService).toConstantValue({ + getDetectedPorts() { + return {}; + }, + async getBoardDetails({ fqbn }) { + return boardDetailsMocks[fqbn]; + }, + }); + bind(NotificationCenter).toSelf().inSingletonScope(); + bind(NotificationServiceServer).toConstantValue(< + NotificationServiceServer + >{ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setClient(_) { + // nothing + }, + }); + bind(FrontendApplicationStateService).toSelf().inSingletonScope(); + bind(BoardsDataStore).toConstantValue({ + async getData(fqbn) { + if (!fqbn) { + return BoardsDataStore.Data.EMPTY; + } + const data = dataStoreMocks[fqbn] ?? BoardsDataStore.Data.EMPTY; + return data; + }, + get onChanged() { + return onDataStoreDidChangeEmitter.event; + }, + }); + bind(LocalStorageService).toSelf().inSingletonScope(); + bind(WindowService).toConstantValue({}); + bind(StorageService).toService(LocalStorageService); + bind(BoardsServiceProvider).toSelf().inSingletonScope(); + bind(NoopHostedPluginSupport).toSelf().inSingletonScope(); + bind(HostedPluginSupport).toService(NoopHostedPluginSupport); + bind(UpdateArduinoState).toSelf().inSingletonScope(); + bind(FileService).toConstantValue({}); + bind(FileSystemExt).toConstantValue({}); + bind(ConfigServiceClient).toConstantValue({ + tryGetSketchDirUri() { + return sketchDirUriMock; + }, + tryGetDataDirUri() { + return dataDirUriMock; + }, + get onDidChangeSketchDirUri() { + return onSketchDirDidChangeEmitter.event; + }, + get onDidChangeDataDirUri() { + return onDataDirDidChangeEmitter.event; + }, + }); + bind(SketchesService).toConstantValue({}); + bind(OpenerService).toConstantValue({}); + bind(SketchesServiceClientImpl).toConstantValue(< + SketchesServiceClientImpl + >{ + tryGetCurrentSketch() { + return currentSketchMock; + }, + onCurrentSketchDidChange: onCurrentSketchDidChangeEmitter.event, + }); + bind(EditorManager).toConstantValue({}); + bind(OutputChannelManager).toConstantValue({}); + bind(EnvVariablesServer).toConstantValue({}); + bind(ApplicationConnectionStatusContribution).toConstantValue( + {} + ); + bind(WorkspaceService).toConstantValue({}); + bind(LabelProvider).toConstantValue({}); + bind(SettingsService).toConstantValue({}); + bind(ArduinoPreferences).toConstantValue({}); + bind(DialogService).toConstantValue({}); + bind(MainMenuManager).toConstantValue({}); + }) + ); + return container; + } +}); + +@injectable() +class NoopHostedPluginSupport implements HostedPluginSupport { + readonly didStart = Promise.resolve(); + readonly onDidCloseConnection = never(); + readonly onDidLoad = never(); +} diff --git a/arduino-ide-extension/src/test/common/common-test-bindings.ts b/arduino-ide-extension/src/test/common/common-test-bindings.ts index 1c1892dc2..f3e3f4373 100644 --- a/arduino-ide-extension/src/test/common/common-test-bindings.ts +++ b/arduino-ide-extension/src/test/common/common-test-bindings.ts @@ -9,14 +9,25 @@ import { LogLevel } from '@theia/core/lib/common/logger-protocol'; import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import { injectable, interfaces } from '@theia/core/shared/inversify'; -export function bindCommon(bind: interfaces.Bind): interfaces.Bind { +export interface Bind { + ( + bind: interfaces.Bind, + unbind: interfaces.Unbind, + isBound: interfaces.IsBound, + rebind: interfaces.Rebind + ): void; +} + +export const bindCommon: Bind = function ( + ...args: Parameters +): ReturnType { + const [bind] = args; bind(ConsoleLogger).toSelf().inSingletonScope(); bind(ILogger).toService(ConsoleLogger); bind(CommandRegistry).toSelf().inSingletonScope(); bind(CommandService).toService(CommandRegistry); bindContributionProvider(bind, CommandContribution); - return bind; -} +}; @injectable() export class ConsoleLogger extends MockLogger { diff --git a/arduino-ide-extension/src/test/node/node-test-bindings.ts b/arduino-ide-extension/src/test/node/node-test-bindings.ts index 2b4c651f7..29ff09f52 100644 --- a/arduino-ide-extension/src/test/node/node-test-bindings.ts +++ b/arduino-ide-extension/src/test/node/node-test-bindings.ts @@ -222,7 +222,7 @@ export async function createBaseContainer( } const container = new Container({ defaultScope: 'Singleton' }); const module = new ContainerModule((bind, unbind, isBound, rebind) => { - bindCommon(bind); + bindCommon(bind, unbind, isBound, rebind); bind(CoreClientProvider).toSelf().inSingletonScope(); bind(CoreServiceImpl).toSelf().inSingletonScope(); bind(CoreService).toService(CoreServiceImpl); diff --git a/arduino-ide-extension/src/test/utils.ts b/arduino-ide-extension/src/test/utils.ts index 799081227..148649126 100644 --- a/arduino-ide-extension/src/test/utils.ts +++ b/arduino-ide-extension/src/test/utils.ts @@ -1,3 +1,6 @@ -export function tick(): Promise { - return new Promise((res) => setTimeout(res, 1)); +import { Emitter, Event } from '@theia/core/lib/common/event'; + +const neverEmitter = new Emitter(); +export function never(): Event { + return neverEmitter.event as Event; }