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..5efc092c3 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -454,6 +454,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board. bind(BoardsDataStore).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(BoardsDataStore); + bind(CommandContribution).toService(BoardsDataStore); + bind(StartupTaskProvider).toService(BoardsDataStore); // to inherit the boards config options, programmer, etc in a new window + // Logger for the Arduino daemon bind(ILogger) .toDynamicValue((ctx) => { diff --git a/arduino-ide-extension/src/browser/boards/boards-data-store.ts b/arduino-ide-extension/src/browser/boards/boards-data-store.ts index 579f30b7a..cd5af3023 100644 --- a/arduino-ide-extension/src/browser/boards/boards-data-store.ts +++ b/arduino-ide-extension/src/browser/boards/boards-data-store.ts @@ -1,21 +1,38 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { StorageService } from '@theia/core/lib/browser/storage-service'; +import type { + Command, + CommandContribution, + CommandRegistry, +} from '@theia/core/lib/common/command'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { ILogger } from '@theia/core/lib/common/logger'; -import { deepClone } from '@theia/core/lib/common/objects'; +import { deepClone, deepFreeze } from '@theia/core/lib/common/objects'; import { inject, injectable, named } from '@theia/core/shared/inversify'; import { BoardDetails, BoardsService, ConfigOption, Programmer, + isBoardIdentifierChangeEvent, } from '../../common/protocol'; import { notEmpty } from '../../common/utils'; +import type { + StartupTask, + StartupTaskProvider, +} from '../../electron-common/startup-task'; import { NotificationCenter } from '../notification-center'; +import { BoardsServiceProvider } from './boards-service-provider'; @injectable() -export class BoardsDataStore implements FrontendApplicationContribution { +export class BoardsDataStore + implements + FrontendApplicationContribution, + StartupTaskProvider, + CommandContribution +{ @inject(ILogger) @named('store') private readonly logger: ILogger; @@ -28,44 +45,110 @@ export class BoardsDataStore implements FrontendApplicationContribution { // In other words, store the data (such as the board configs) per sketch, not per IDE2 installation. https://github.com/arduino/arduino-ide/issues/2240 @inject(StorageService) private readonly storageService: StorageService; + @inject(BoardsServiceProvider) + private readonly boardsServiceProvider: BoardsServiceProvider; + @inject(FrontendApplicationStateService) + private readonly appStateService: FrontendApplicationStateService; - private readonly onChangedEmitter = new Emitter(); - private readonly toDispose = new DisposableCollection(this.onChangedEmitter); + private readonly onDidChangeEmitter = + new Emitter(); + private readonly toDispose = new DisposableCollection( + this.onDidChangeEmitter + ); + private _selectedBoardData: BoardsDataStoreChange | undefined; onStart(): void { - this.toDispose.push( + this.toDispose.pushAll([ + this.boardsServiceProvider.onBoardsConfigDidChange((event) => { + if (isBoardIdentifierChangeEvent(event)) { + this.updateSelectedBoardData(event.selectedBoard?.fqbn); + } + }), this.notificationCenter.onPlatformDidInstall(async ({ item }) => { - const dataDidChangePerFqbn: string[] = []; - for (const fqbn of item.boards + const boardsWithFqbn = item.boards .map(({ fqbn }) => fqbn) - .filter(notEmpty) - .filter((fqbn) => !!fqbn)) { + .filter(notEmpty); + const changes: BoardsDataStoreChange[] = []; + for (const fqbn of boardsWithFqbn) { const key = this.getStorageKey(fqbn); - let data = await this.storageService.getData(key); - if (!data || !data.length) { - const details = await this.getBoardDetailsSafe(fqbn); - if (details) { - data = details.configOptions; - if (data.length) { - await this.storageService.setData(key, data); - dataDidChangePerFqbn.push(fqbn); - } - } + const storedData = + await this.storageService.getData(key); + if (!storedData) { + // if not previously value is available for the board, do not update the cache + continue; + } + const details = await this.getBoardDetailsSafe(fqbn); + if (details) { + const data = createDataStoreEntry(details); + await this.storageService.setData(key, data); + changes.push({ fqbn, data }); } } - if (dataDidChangePerFqbn.length) { - this.fireChanged(...dataDidChangePerFqbn); + if (changes.length) { + this.fireChanged(...changes); } - }) + }), + ]); + + Promise.all([ + this.boardsServiceProvider.ready, + this.appStateService.reachedState('ready'), + ]).then(() => + this.updateSelectedBoardData( + this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn + ) ); } + private async getSelectedBoardData( + fqbn: string | undefined + ): Promise { + if (!fqbn) { + return undefined; + } else { + const data = await this.getData(fqbn); + if (data === BoardsDataStore.Data.EMPTY) { + return undefined; + } + return { fqbn, data }; + } + } + + private async updateSelectedBoardData( + fqbn: string | undefined + ): Promise { + this._selectedBoardData = await this.getSelectedBoardData(fqbn); + } + onStop(): void { this.toDispose.dispose(); } - get onChanged(): Event { - return this.onChangedEmitter.event; + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(USE_INHERITED_DATA, { + execute: async (arg: unknown) => { + if (isBoardsDataStoreChange(arg)) { + await this.setData(arg); + this.fireChanged(arg); + } + }, + }); + } + + tasks(): StartupTask[] { + if (!this._selectedBoardData) { + return []; + } + return [ + { + command: USE_INHERITED_DATA.id, + args: [this._selectedBoardData], + }, + ]; + } + + get onDidChange(): Event { + return this.onDidChangeEmitter.event; } async appendConfigToFqbn( @@ -84,11 +167,11 @@ export class BoardsDataStore implements FrontendApplicationContribution { } const key = this.getStorageKey(fqbn); - let data = await this.storageService.getData< + const storedData = await this.storageService.getData< BoardsDataStore.Data | undefined >(key, undefined); - if (BoardsDataStore.Data.is(data)) { - return data; + if (BoardsDataStore.Data.is(storedData)) { + return storedData; } const boardDetails = await this.getBoardDetailsSafe(fqbn); @@ -96,10 +179,7 @@ export class BoardsDataStore implements FrontendApplicationContribution { return BoardsDataStore.Data.EMPTY; } - data = { - configOptions: boardDetails.configOptions, - programmers: boardDetails.programmers, - }; + const data = createDataStoreEntry(boardDetails); await this.storageService.setData(key, data); return data; } @@ -111,17 +191,15 @@ export class BoardsDataStore implements FrontendApplicationContribution { fqbn: string; selectedProgrammer: Programmer; }): Promise { - const data = deepClone(await this.getData(fqbn)); - const { programmers } = data; + const storedData = deepClone(await this.getData(fqbn)); + const { programmers } = storedData; if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) { return false; } - await this.setData({ - fqbn, - data: { ...data, selectedProgrammer }, - }); - this.fireChanged(fqbn); + const data = { ...storedData, selectedProgrammer }; + await this.setData({ fqbn, data }); + this.fireChanged({ fqbn, data }); return true; } @@ -153,17 +231,12 @@ export class BoardsDataStore implements FrontendApplicationContribution { return false; } await this.setData({ fqbn, data }); - this.fireChanged(fqbn); + this.fireChanged({ fqbn, data }); return true; } - protected async setData({ - fqbn, - data, - }: { - fqbn: string; - data: BoardsDataStore.Data; - }): Promise { + protected async setData(change: BoardsDataStoreChange): Promise { + const { fqbn, data } = change; const key = this.getStorageKey(fqbn); return this.storageService.setData(key, data); } @@ -176,7 +249,7 @@ export class BoardsDataStore implements FrontendApplicationContribution { fqbn: string ): Promise { try { - const details = this.boardsService.getBoardDetails({ fqbn }); + const details = await this.boardsService.getBoardDetails({ fqbn }); return details; } catch (err) { if ( @@ -197,8 +270,8 @@ export class BoardsDataStore implements FrontendApplicationContribution { } } - protected fireChanged(...fqbn: string[]): void { - this.onChangedEmitter.fire(fqbn); + protected fireChanged(...changes: BoardsDataStoreChange[]): void { + this.onDidChangeEmitter.fire({ changes }); } } @@ -209,10 +282,10 @@ export namespace BoardsDataStore { readonly selectedProgrammer?: Programmer; } export namespace Data { - export const EMPTY: Data = { + export const EMPTY: Data = deepFreeze({ configOptions: [], programmers: [], - }; + }); export function is(arg: any): arg is Data { return ( !!arg && @@ -224,3 +297,32 @@ export namespace BoardsDataStore { } } } + +function createDataStoreEntry(details: BoardDetails): BoardsDataStore.Data { + return { + configOptions: details.configOptions.slice(), + programmers: details.programmers.slice(), + }; +} + +export interface BoardsDataStoreChange { + readonly fqbn: string; + readonly data: BoardsDataStore.Data; +} + +function isBoardsDataStoreChange(arg: unknown): arg is BoardsDataStoreChange { + return ( + typeof arg === 'object' && + arg !== null && + typeof (arg).fqbn === 'string' && + BoardsDataStore.Data.is((arg).data) + ); +} + +export interface BoardsDataStoreChangeEvent { + readonly changes: readonly BoardsDataStoreChange[]; +} + +const USE_INHERITED_DATA: Command = { + id: 'arduino-use-inherited-boards-data', +}; diff --git a/arduino-ide-extension/src/browser/contributions/boards-data-menu-updater.ts b/arduino-ide-extension/src/browser/contributions/boards-data-menu-updater.ts index d9fe0ae7e..ea085f5ba 100644 --- a/arduino-ide-extension/src/browser/contributions/boards-data-menu-updater.ts +++ b/arduino-ide-extension/src/browser/contributions/boards-data-menu-updater.ts @@ -35,7 +35,7 @@ export class BoardsDataMenuUpdater extends Contribution { private readonly toDisposeOnBoardChange = new DisposableCollection(); override onStart(): void { - this.boardsDataStore.onChanged(() => + this.boardsDataStore.onDidChange(() => this.updateMenuActions( this.boardsServiceProvider.boardsConfig.selectedBoard ) diff --git a/arduino-ide-extension/src/browser/contributions/ino-language.ts b/arduino-ide-extension/src/browser/contributions/ino-language.ts index 096c27ed8..26c7487d1 100644 --- a/arduino-ide-extension/src/browser/contributions/ino-language.ts +++ b/arduino-ide-extension/src/browser/contributions/ino-language.ts @@ -90,7 +90,7 @@ export class InoLanguage extends SketchContribution { this.notificationCenter.onPlatformDidInstall(() => forceRestart()), this.notificationCenter.onPlatformDidUninstall(() => forceRestart()), this.notificationCenter.onDidReinitialize(() => forceRestart()), - this.boardDataStore.onChanged((dataChangePerFqbn) => { + this.boardDataStore.onDidChange((event) => { if (this.languageServerFqbn) { const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn); if (!sanitizeFqbn) { @@ -98,13 +98,13 @@ export class InoLanguage extends SketchContribution { `Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}` ); } - const matchingFqbn = dataChangePerFqbn.find( - (fqbn) => sanitizedFqbn === fqbn + const matchingChange = event.changes.find( + (change) => change.fqbn === sanitizedFqbn ); const { boardsConfig } = this.boardsServiceProvider; if ( - matchingFqbn && - boardsConfig.selectedBoard?.fqbn === matchingFqbn + matchingChange && + boardsConfig.selectedBoard?.fqbn === matchingChange.fqbn ) { start(boardsConfig.selectedBoard); } 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..ab4cdafb0 100644 --- a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts +++ b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts @@ -65,10 +65,13 @@ export class UpdateArduinoState extends SketchContribution { this.updateCompileSummary(args[0]); } }), - this.boardsDataStore.onChanged((fqbn) => { + this.boardsDataStore.onDidChange((event) => { const selectedFqbn = this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn; - if (selectedFqbn && fqbn.includes(selectedFqbn)) { + if ( + selectedFqbn && + event.changes.find((change) => change.fqbn === selectedFqbn) + ) { this.updateBoardDetails(selectedFqbn); } }), diff --git a/arduino-ide-extension/src/test/browser/boards-data-store.test.ts b/arduino-ide-extension/src/test/browser/boards-data-store.test.ts new file mode 100644 index 000000000..4f52c207d --- /dev/null +++ b/arduino-ide-extension/src/test/browser/boards-data-store.test.ts @@ -0,0 +1,501 @@ +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 { + 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 { MessageService } from '@theia/core/lib/common/message-service'; +import { wait } from '@theia/core/lib/common/promise-util'; +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'; +import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider'; +import { NotificationCenter } from '../../browser/notification-center'; +import { + BoardDetails, + BoardsPackage, + BoardsService, + ConfigOption, + Programmer, +} from '../../common/protocol/boards-service'; +import { NotificationServiceServer } from '../../common/protocol/notification-service'; +import { ConsoleLogger, bindCommon } from '../common/common-test-bindings'; + +disableJSDOM(); + +describe('boards-data-store', function () { + this.slow(250); + + let toDisposeAfterEach: DisposableCollection; + let boardsServiceProvider: BoardsServiceProvider; + let boardsDataStore: BoardsDataStore; + let notificationCenter: NotificationCenter; + + beforeEach(async () => { + const container = createContainer(); + container.get( + FrontendApplicationStateService + ).state = 'ready'; + notificationCenter = container.get(NotificationCenter); + boardsServiceProvider = container.get( + BoardsServiceProvider + ); + toDisposeAfterEach = new DisposableCollection( + Disposable.create(() => boardsServiceProvider.onStop()) + ); + boardsServiceProvider.onStart(); + await boardsServiceProvider.ready; + boardsDataStore = container.get(BoardsDataStore); + boardsDataStore.onStart(); + }); + + afterEach(() => toDisposeAfterEach.dispose()); + + it('should load the board details when absent in local storage', async () => { + const storedData = await getStoredData(fqbn); + expect(storedData).to.be.undefined; + const data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + }); + + it('should load from local storage if present', async () => { + const storedData: BoardsDataStore.Data = { + configOptions: [], + programmers: [edbg], + selectedProgrammer: edbg, + }; + await setStorageData(fqbn, storedData); + const data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal(storedData); + }); + + it('should update board details of selected board (selected with FQBN)', async () => { + const updated = boardsServiceProvider.updateConfig(board); + expect(updated).to.be.ok; + await wait(50); + + const selectedBoardData = boardsDataStore['_selectedBoardData']; + expect(selectedBoardData).to.be.deep.equal({ + fqbn, + data: { + configOptions: [configOption1], + programmers: [edbg, jlink], + }, + }); + }); + + it('should not update the board details of selected board when FQBN is missing', async () => { + const fqbn = undefined; + const name = 'ABC'; + const board = { name, fqbn }; + const updated = boardsServiceProvider.updateConfig(board); + expect(updated).to.ok; + await wait(50); + + const selectedBoardData = boardsDataStore['_selectedBoardData']; + expect(selectedBoardData).to.be.undefined; + }); + + it('should unset the the board details of selected board when no board was selected', async () => { + let updated = boardsServiceProvider.updateConfig(board); + expect(updated).to.ok; + await wait(50); + + let selectedBoardData = boardsDataStore['_selectedBoardData']; + expect(selectedBoardData).to.be.deep.equal({ + fqbn, + data: { + configOptions: [configOption1], + programmers: [edbg, jlink], + }, + }); + + updated = boardsServiceProvider.updateConfig('unset-board'); + expect(updated).to.be.true; + await wait(50); + + selectedBoardData = boardsDataStore['_selectedBoardData']; + expect(selectedBoardData).to.be.undefined; + }); + + it('should provide startup tasks when the data is available for the selected board', async () => { + const updated = boardsServiceProvider.updateConfig(board); + expect(updated).to.be.true; + await wait(50); + + const tasks = boardsDataStore.tasks(); + expect(tasks).to.be.deep.equal([ + { + command: 'arduino-use-inherited-boards-data', + args: [ + { + fqbn, + data: { + configOptions: [configOption1], + programmers: [edbg, jlink], + }, + }, + ], + }, + ]); + }); + + it('should not provide any startup tasks when no data is available for the selected board', async () => { + const tasks = boardsDataStore.tasks(); + expect(tasks).to.be.empty; + }); + + it('should select a programmer', async () => { + let data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + const result = await boardsDataStore.selectProgrammer({ + fqbn, + selectedProgrammer: edbg, + }); + expect(result).to.be.ok; + expect(didChangeCounter).to.be.equal(1); + + data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + selectedProgrammer: edbg, + }); + }); + + it('should not select a programmer if it is absent', async () => { + let data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + const result = await boardsDataStore.selectProgrammer({ + fqbn, + selectedProgrammer: { id: 'p1', name: 'P1', platform: 'missing' }, + }); + expect(result).to.be.not.ok; + expect(didChangeCounter).to.be.equal(0); + + data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + }); + + it('should select a config option', async () => { + let data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + const result = await boardsDataStore.selectConfigOption({ + fqbn, + option: configOption1.option, + selectedValue: configOption1.values[1].value, + }); + expect(result).to.be.ok; + expect(didChangeCounter).to.be.equal(1); + + data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [ + { + ...configOption1, + values: [ + { label: 'C1V1', selected: false, value: 'v1' }, + { label: 'C1V2', selected: true, value: 'v2' }, + ], + }, + ], + programmers: [edbg, jlink], + }); + }); + + it('should not select a config option if the option is absent', async () => { + const fqbn = 'a:b:c'; + let data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + const result = await boardsDataStore.selectConfigOption({ + fqbn, + option: 'missing', + selectedValue: configOption1.values[1].value, + }); + expect(result).to.be.not.ok; + expect(didChangeCounter).to.be.equal(0); + + data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + }); + + it('should not select a config option if the selected value is absent', async () => { + let data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + const result = await boardsDataStore.selectConfigOption({ + fqbn, + option: configOption1.option, + selectedValue: 'missing', + }); + expect(result).to.be.not.ok; + expect(didChangeCounter).to.be.equal(0); + + data = await boardsDataStore.getData(fqbn); + expect(data).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + }); + + it('should not update the board data on platform install if it was not cached', async () => { + let storedData = await getStoredData(fqbn); + expect(storedData).to.be.undefined; + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + notificationCenter.notifyPlatformDidInstall({ item: boardsPackage }); + await wait(50); + expect(didChangeCounter).to.be.equal(0); + + storedData = await getStoredData(fqbn); + expect(storedData).to.be.undefined; + }); + + it('should update the board data on platform install if the default empty value was cached', async () => { + let storedData = await getStoredData(fqbn); + expect(storedData).to.be.undefined; + + await setStorageData(fqbn, BoardsDataStore.Data.EMPTY); + storedData = await getStoredData(fqbn); + expect(storedData).to.be.deep.equal(BoardsDataStore.Data.EMPTY); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + notificationCenter.notifyPlatformDidInstall({ item: boardsPackage }); + await wait(50); + expect(didChangeCounter).to.be.equal(1); + + storedData = await getStoredData(fqbn); + expect(storedData).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + }); + + it('should update the cached board data on platform install', async () => { + let storedData = await boardsDataStore.getData(fqbn); // caches the value + expect(storedData).to.be.deep.equal({ + configOptions: [configOption1], + programmers: [edbg, jlink], + }); + + // before the platform install event mock a different CLI `board details` output + toDisposeAfterEach.push( + mockBoardDetails([ + { + fqbn, + ...baseDetails, + configOptions: [configOption2], + }, + ]) + ); + + let didChangeCounter = 0; + toDisposeAfterEach.push( + boardsDataStore.onDidChange(() => didChangeCounter++) + ); + notificationCenter.notifyPlatformDidInstall({ item: boardsPackage }); + await wait(50); + expect(didChangeCounter).to.be.equal(1); + + storedData = await boardsDataStore.getData(fqbn); + expect(storedData).to.be.deep.equal({ + configOptions: [configOption2], + programmers: [edbg, jlink], + }); + }); + + function storageKey(fqbn: string): string { + return boardsDataStore['getStorageKey'](fqbn); + } + + function getStoredData(fqbn: string): Promise { + const key = storageKey(fqbn); + return boardsDataStore['storageService'].getData(key); + } + + function setStorageData( + fqbn: string, + data: BoardsDataStore.Data + ): Promise { + const key = storageKey(fqbn); + return boardsDataStore['storageService'].setData(key, data); + } + + function createContainer(): Container { + const container = new Container({ defaultScope: 'Singleton' }); + container.load( + new ContainerModule((bind, unbind, isBound, rebind) => { + bindCommon(bind); + bind(MessageService).toConstantValue({}); + bind(BoardsService).toConstantValue({ + getDetectedPorts() { + return {}; + }, + async getBoardDetails({ fqbn }) { + return boardDetailsMock().find((mock) => mock.fqbn === 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).toSelf().inSingletonScope(); + bind(LocalStorageService).toSelf().inSingletonScope(); + 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; + } + + // Mocks the CLI's `board details` response + const jlink: Programmer = { + platform: 'Arduino SAMD (32-bits ARM Cortex-M0+) Boards', + id: 'jlink', + name: 'Segger J-Link', + }; + const edbg: Programmer = { + platform: 'Arduino SAMD (32-bits ARM Cortex-M0+) Boards', + id: 'edbg', + name: 'Atmel EDBG', + }; + + const configOption1: ConfigOption = { + label: 'C1', + option: 'c1', + values: [ + { label: 'C1V1', selected: true, value: 'v1' }, + { label: 'C1V2', selected: false, value: 'v2' }, + ], + }; + + const configOption2: ConfigOption = { + label: 'C2', + option: 'c2', + values: [ + { label: 'C2V1', selected: true, value: 'v1' }, + { label: 'C2V2', selected: false, value: 'v2' }, + ], + }; + + const baseDetails: Omit = { + VID: '1', + PID: '1', + buildProperties: [], + configOptions: [configOption1], + debuggingSupported: false, + programmers: [edbg, jlink], + requiredTools: [], + }; + + const fqbn = 'a:b:c'; + const name = 'ABC'; + const board = { fqbn, name }; + + const boardsPackage: BoardsPackage = { + id: 'a:b', + name: 'AB', + availableVersions: ['1.0.0'], + boards: [board], + description: 'boy', + summary: ':heart:', + author: 'mano', + types: [], + }; + + const defaultDetailsMocks: readonly BoardDetails[] = [ + { + fqbn, + ...baseDetails, + }, + ]; + let _currentDetailsMock = defaultDetailsMocks; + + function boardDetailsMock(): readonly BoardDetails[] { + return _currentDetailsMock; + } + function mockBoardDetails(newDetails: BoardDetails[]): Disposable { + _currentDetailsMock = newDetails; + return Disposable.create(resetDetailsMock); + } + function resetDetailsMock(): void { + _currentDetailsMock = defaultDetailsMocks; + } +});