mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-04-19 12:57:17 +00:00
feat: new window inherits the custom board options
A new startup task ensures setting any custom board menu selection in a new sketch window. Closes #2271 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
0f83a48649
commit
633346a3b0
@ -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) => {
|
||||
|
@ -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<string[]>();
|
||||
private readonly toDispose = new DisposableCollection(this.onChangedEmitter);
|
||||
private readonly onDidChangeEmitter =
|
||||
new Emitter<BoardsDataStoreChangeEvent>();
|
||||
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<ConfigOption[]>(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<BoardsDataStore.Data>(key);
|
||||
if (!storedData) {
|
||||
// if not previously value is available for the board, do not update the cache
|
||||
continue;
|
||||
}
|
||||
const details = await this.loadBoardDetails(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<BoardsDataStoreChange | undefined> {
|
||||
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<void> {
|
||||
this._selectedBoardData = await this.getSelectedBoardData(fqbn);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get onChanged(): Event<string[]> {
|
||||
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<BoardsDataStoreChangeEvent> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
protected async setData(change: BoardsDataStoreChange): Promise<void> {
|
||||
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<BoardDetails | undefined> {
|
||||
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,11 +282,13 @@ 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 {
|
||||
defaultProgrammerId: undefined,
|
||||
});
|
||||
|
||||
export function is(arg: unknown): arg is Data {
|
||||
return (
|
||||
!!arg &&
|
||||
'configOptions' in arg &&
|
||||
@ -224,3 +299,61 @@ export namespace BoardsDataStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isEmptyData(data: BoardsDataStore.Data): boolean {
|
||||
return (
|
||||
Boolean(!data.configOptions.length) &&
|
||||
Boolean(!data.programmers.length) &&
|
||||
Boolean(!data.selectedProgrammer)
|
||||
);
|
||||
}
|
||||
|
||||
export function findDefaultProgrammer(
|
||||
programmers: readonly Programmer[],
|
||||
defaultProgrammerId: string | undefined | BoardsDataStore.Data
|
||||
): Programmer | undefined {
|
||||
if (!defaultProgrammerId) {
|
||||
return undefined;
|
||||
}
|
||||
const id =
|
||||
typeof defaultProgrammerId === 'string'
|
||||
? defaultProgrammerId
|
||||
: defaultProgrammerId.defaultProgrammerId;
|
||||
return programmers.find((p) => p.id === id);
|
||||
}
|
||||
function createDataStoreEntry(details: BoardDetails): BoardsDataStore.Data {
|
||||
const configOptions = details.configOptions.slice();
|
||||
const programmers = details.programmers.slice();
|
||||
const selectedProgrammer = findDefaultProgrammer(
|
||||
programmers,
|
||||
details.defaultProgrammerId
|
||||
);
|
||||
return {
|
||||
configOptions,
|
||||
programmers,
|
||||
defaultProgrammerId: details.defaultProgrammerId,
|
||||
selectedProgrammer,
|
||||
};
|
||||
}
|
||||
|
||||
export interface BoardsDataStoreChange {
|
||||
readonly fqbn: string;
|
||||
readonly data: BoardsDataStore.Data;
|
||||
}
|
||||
|
||||
function isBoardsDataStoreChange(arg: unknown): arg is BoardsDataStoreChange {
|
||||
return (
|
||||
typeof arg === 'object' &&
|
||||
arg !== null &&
|
||||
typeof (<BoardsDataStoreChange>arg).fqbn === 'string' &&
|
||||
BoardsDataStore.Data.is((<BoardsDataStoreChange>arg).data)
|
||||
);
|
||||
}
|
||||
|
||||
export interface BoardsDataStoreChangeEvent {
|
||||
readonly changes: readonly BoardsDataStoreChange[];
|
||||
}
|
||||
|
||||
const USE_INHERITED_DATA: Command = {
|
||||
id: 'arduino-use-inherited-boards-data',
|
||||
};
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}),
|
||||
|
501
arduino-ide-extension/src/test/browser/boards-data-store.test.ts
Normal file
501
arduino-ide-extension/src/test/browser/boards-data-store.test.ts
Normal file
@ -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>(
|
||||
FrontendApplicationStateService
|
||||
).state = 'ready';
|
||||
notificationCenter = container.get<NotificationCenter>(NotificationCenter);
|
||||
boardsServiceProvider = container.get<BoardsServiceProvider>(
|
||||
BoardsServiceProvider
|
||||
);
|
||||
toDisposeAfterEach = new DisposableCollection(
|
||||
Disposable.create(() => boardsServiceProvider.onStop())
|
||||
);
|
||||
boardsServiceProvider.onStart();
|
||||
await boardsServiceProvider.ready;
|
||||
boardsDataStore = container.get<BoardsDataStore>(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<unknown> {
|
||||
const key = storageKey(fqbn);
|
||||
return boardsDataStore['storageService'].getData(key);
|
||||
}
|
||||
|
||||
function setStorageData(
|
||||
fqbn: string,
|
||||
data: BoardsDataStore.Data
|
||||
): Promise<void> {
|
||||
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(<MessageService>{});
|
||||
bind(BoardsService).toConstantValue(<BoardsService>{
|
||||
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(<WindowService>{});
|
||||
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<BoardDetails, 'fqbn'> = {
|
||||
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;
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user