implement unit tests for boards-auto-installer (#513)

Co-authored-by: Francesco Stasi <f.stasi@me.com>
This commit is contained in:
Alberto Iannaccone
2021-09-27 10:09:11 +01:00
committed by GitHub
parent 79b075c961
commit e9db1c0482
14 changed files with 583 additions and 36 deletions

View File

@@ -4,7 +4,7 @@
"description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later",
"scripts": {
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn clean && yarn download-examples && yarn build",
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn clean && yarn download-examples && yarn build && yarn test",
"clean": "rimraf lib",
"download-cli": "node ./scripts/download-cli.js",
"download-fwuploader": "node ./scripts/download-fwuploader.js",
@@ -101,6 +101,7 @@
"protoc": "^1.0.4",
"shelljs": "^0.8.3",
"sinon": "^9.0.1",
"typemoq": "^2.1.0",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
},
@@ -109,7 +110,8 @@
},
"mocha": {
"require": [
"reflect-metadata/Reflect"
"reflect-metadata/Reflect",
"ignore-styles"
],
"reporter": "spec",
"colors": true,

View File

@@ -165,8 +165,9 @@ import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/mo
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
import { ResponseServiceImpl } from './response-service-impl';
import {
ResponseServicePath,
ResponseService,
ResponseServiceArduino,
ResponseServicePath,
} from '../common/protocol/response-service';
import { NotificationCenter } from './notification-center';
import {
@@ -617,7 +618,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
);
return responseService;
});
bind(ResponseService).toService(ResponseServiceImpl);
bind(ResponseServiceArduino).toService(ResponseServiceImpl);
bind(NotificationCenter).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(NotificationCenter);

View File

@@ -7,10 +7,9 @@ import {
Board,
} from '../../common/protocol/boards-service';
import { BoardsServiceProvider } from './boards-service-provider';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { BoardsConfig } from './boards-config';
import { Installable } from '../../common/protocol';
import { ResponseServiceImpl } from '../response-service-impl';
import { Installable, ResponseServiceArduino } from '../../common/protocol';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
/**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
@@ -27,8 +26,8 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider;
@inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl;
@inject(ResponseServiceArduino)
protected readonly responseService: ResponseServiceArduino;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
@@ -106,7 +105,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
});
return;
}
if (answer) {
if (answer === 'Install Manually') {
this.boardsManagerFrontendContribution
.openView({ reveal: true })
.then((widget) =>

View File

@@ -4,8 +4,11 @@ import URI from '@theia/core/lib/common/uri';
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ArduinoMenus } from '../menu/arduino-menus';
import { ResponseServiceImpl } from '../response-service-impl';
import { Installable, LibraryService } from '../../common/protocol';
import {
Installable,
LibraryService,
ResponseServiceArduino,
} from '../../common/protocol';
import {
SketchContribution,
Command,
@@ -18,8 +21,8 @@ export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer)
protected readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl;
@inject(ResponseServiceArduino)
protected readonly responseService: ResponseServiceArduino;
@inject(LibraryService)
protected readonly libraryService: LibraryService;

View File

@@ -3,13 +3,13 @@ import { Emitter } from '@theia/core/lib/common/event';
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
import {
ResponseService,
OutputMessage,
ProgressMessage,
ResponseServiceArduino,
} from '../common/protocol/response-service';
@injectable()
export class ResponseServiceImpl implements ResponseService {
export class ResponseServiceImpl implements ResponseServiceArduino {
@inject(OutputContribution)
protected outputContribution: OutputContribution;
@@ -17,8 +17,13 @@ export class ResponseServiceImpl implements ResponseService {
protected outputChannelManager: OutputChannelManager;
protected readonly progressDidChangeEmitter = new Emitter<ProgressMessage>();
readonly onProgressDidChange = this.progressDidChangeEmitter.event;
clearArduinoChannel(): void {
this.outputChannelManager.getChannel('Arduino').clear();
}
appendToOutput(message: OutputMessage): void {
const { chunk } = message;
const channel = this.outputChannelManager.getChannel('Arduino');
@@ -26,10 +31,6 @@ export class ResponseServiceImpl implements ResponseService {
channel.append(chunk);
}
clearArduinoChannel(): void {
this.outputChannelManager.getChannel('Arduino').clear();
}
reportProgress(progress: ProgressMessage): void {
this.progressDidChangeEmitter.fire(progress);
}

View File

@@ -11,7 +11,7 @@ import { SearchBar } from './search-bar';
import { ListWidget } from './list-widget';
import { ComponentList } from './component-list';
import { ListItemRenderer } from './list-item-renderer';
import { ResponseServiceImpl } from '../../response-service-impl';
import { ResponseServiceArduino } from '../../../common/protocol';
export class FilterableListContainer<
T extends ArduinoComponent
@@ -153,7 +153,7 @@ export namespace FilterableListContainer {
readonly resolveFocus: (element: HTMLElement | undefined) => void;
readonly filterTextChangeEvent: Event<string | undefined>;
readonly messageService: MessageService;
readonly responseService: ResponseServiceImpl;
readonly responseService: ResponseServiceArduino;
readonly install: ({
item,
progressId,

View File

@@ -12,11 +12,11 @@ import {
Installable,
Searchable,
ArduinoComponent,
ResponseServiceArduino,
} from '../../../common/protocol';
import { FilterableListContainer } from './filterable-list-container';
import { ListItemRenderer } from './list-item-renderer';
import { NotificationCenter } from '../../notification-center';
import { ResponseServiceImpl } from '../../response-service-impl';
@injectable()
export abstract class ListWidget<
@@ -28,8 +28,8 @@ export abstract class ListWidget<
@inject(CommandService)
protected readonly commandService: CommandService;
@inject(ResponseServiceImpl)
protected readonly responseService: ResponseServiceImpl;
@inject(ResponseServiceArduino)
protected readonly responseService: ResponseServiceArduino;
@inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter;

View File

@@ -7,7 +7,7 @@ import {
import { naturalCompare } from './../utils';
import { ArduinoComponent } from './arduino-component';
import { MessageService } from '@theia/core';
import { ResponseServiceImpl } from '../../browser/response-service-impl';
import { ResponseServiceArduino } from './response-service';
export interface Installable<T extends ArduinoComponent> {
/**
@@ -44,7 +44,7 @@ export namespace Installable {
>(options: {
installable: Installable<T>;
messageService: MessageService;
responseService: ResponseServiceImpl;
responseService: ResponseServiceArduino;
item: T;
version: Installable.Version;
}): Promise<void> {
@@ -66,7 +66,7 @@ export namespace Installable {
>(options: {
installable: Installable<T>;
messageService: MessageService;
responseService: ResponseServiceImpl;
responseService: ResponseServiceArduino;
item: T;
}): Promise<void> {
const { item } = options;
@@ -86,7 +86,7 @@ export namespace Installable {
export async function doWithProgress(options: {
run: ({ progressId }: { progressId: string }) => Promise<void>;
messageService: MessageService;
responseService: ResponseServiceImpl;
responseService: ResponseServiceArduino;
progressText: string;
}): Promise<void> {
return withProgress(

View File

@@ -1,3 +1,5 @@
import { Event } from '@theia/core/lib/common/event';
export interface OutputMessage {
readonly chunk: string;
readonly severity?: 'error' | 'warning' | 'info'; // Currently not used!
@@ -21,3 +23,9 @@ export interface ResponseService {
appendToOutput(message: OutputMessage): void;
reportProgress(message: ProgressMessage): void;
}
export const ResponseServiceArduino = Symbol('ResponseServiceArduino');
export interface ResponseServiceArduino extends ResponseService {
onProgressDidChange: Event<ProgressMessage>;
clearArduinoChannel: () => void;
}

View File

@@ -0,0 +1,247 @@
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
const disableJSDOM = enableJSDOM();
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { ApplicationProps } from '@theia/application-package/lib/application-props';
FrontendApplicationConfigProvider.set({
...ApplicationProps.DEFAULT.frontend.config,
});
import { MessageService } from '@theia/core';
import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider';
import { BoardsListWidgetFrontendContribution } from '../../browser/boards/boards-widget-frontend-contribution';
import {
Board,
BoardsPackage,
BoardsService,
Port,
ResponseServiceArduino,
} from '../../common/protocol';
import { IMock, It, Mock, Times } from 'typemoq';
import { Container, ContainerModule } from 'inversify';
import { BoardsAutoInstaller } from '../../browser/boards/boards-auto-installer';
import { BoardsConfig } from '../../browser/boards/boards-config';
import { tick } from '../utils';
import { ListWidget } from '../../browser/widgets/component-list/list-widget';
disableJSDOM();
const aBoard: Board = {
fqbn: 'some:board:fqbn',
name: 'Some Arduino Board',
port: { address: '/lol/port1234', protocol: 'serial' },
};
const aPort: Port = {
address: aBoard.port!.address,
protocol: aBoard.port!.protocol,
};
const aBoardConfig: BoardsConfig.Config = {
selectedBoard: aBoard,
selectedPort: aPort,
};
const aPackage: BoardsPackage = {
author: 'someAuthor',
availableVersions: ['some.ver.sion', 'some.other.version'],
boards: [aBoard],
deprecated: false,
description: 'Some Arduino Board, Some Other Arduino Board',
id: 'some:arduinoCoreId',
installable: true,
moreInfoLink: 'http://www.some-url.lol/',
name: 'Some Arduino Package',
summary: 'Boards included in this package:',
};
const anInstalledPackage: BoardsPackage = {
...aPackage,
installedVersion: 'some.ver.sion',
};
describe('BoardsAutoInstaller', () => {
let subject: BoardsAutoInstaller;
let messageService: IMock<MessageService>;
let boardsService: IMock<BoardsService>;
let boardsServiceClient: IMock<BoardsServiceProvider>;
let responseService: IMock<ResponseServiceArduino>;
let boardsManagerFrontendContribution: IMock<BoardsListWidgetFrontendContribution>;
let boardsManagerWidget: IMock<ListWidget<BoardsPackage>>;
let testContainer: Container;
beforeEach(() => {
testContainer = new Container();
messageService = Mock.ofType<MessageService>();
boardsService = Mock.ofType<BoardsService>();
boardsServiceClient = Mock.ofType<BoardsServiceProvider>();
responseService = Mock.ofType<ResponseServiceArduino>();
boardsManagerFrontendContribution =
Mock.ofType<BoardsListWidgetFrontendContribution>();
boardsManagerWidget = Mock.ofType<ListWidget<BoardsPackage>>();
boardsManagerWidget.setup((b) =>
b.refresh(aPackage.name.toLocaleLowerCase())
);
boardsManagerFrontendContribution
.setup((b) => b.openView({ reveal: true }))
.returns(async () => boardsManagerWidget.object);
messageService
.setup((m) => m.showProgress(It.isAny(), It.isAny()))
.returns(async () => ({
cancel: () => null,
id: '',
report: () => null,
result: Promise.resolve(''),
}));
responseService
.setup((r) => r.onProgressDidChange(It.isAny()))
.returns(() => ({ dispose: () => null }));
const module = new ContainerModule((bind) => {
bind(BoardsAutoInstaller).toSelf();
bind(MessageService).toConstantValue(messageService.object);
bind(BoardsService).toConstantValue(boardsService.object);
bind(BoardsServiceProvider).toConstantValue(boardsServiceClient.object);
bind(ResponseServiceArduino).toConstantValue(responseService.object);
bind(BoardsListWidgetFrontendContribution).toConstantValue(
boardsManagerFrontendContribution.object
);
});
testContainer.load(module);
subject = testContainer.get(BoardsAutoInstaller);
});
context('when it starts', () => {
it('should register to the BoardsServiceClient in order to check the packages every a new board is plugged in', () => {
subject.onStart();
boardsServiceClient.verify(
(b) => b.onBoardsConfigChanged(It.isAny()),
Times.once()
);
});
context('and it checks the installable packages', () => {
context(`and a port and a board a selected`, () => {
beforeEach(() => {
boardsServiceClient
.setup((b) => b.boardsConfig)
.returns(() => aBoardConfig);
});
context('if no package for the board is already installed', () => {
context('if a candidate package for the board is found', () => {
beforeEach(() => {
boardsService
.setup((b) => b.search(It.isValue({})))
.returns(async () => [aPackage]);
});
it('should show a notification suggesting to install that package', async () => {
messageService
.setup((m) =>
m.info(It.isAnyString(), It.isAnyString(), It.isAnyString())
)
.returns(() => Promise.resolve('Install Manually'));
subject.onStart();
await tick();
messageService.verify(
(m) =>
m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()),
Times.once()
);
});
context(`if the answer to the message is 'Yes'`, () => {
beforeEach(() => {
messageService
.setup((m) =>
m.info(It.isAnyString(), It.isAnyString(), It.isAnyString())
)
.returns(() => Promise.resolve('Yes'));
});
it('should install the package', async () => {
subject.onStart();
await tick();
messageService.verify(
(m) => m.showProgress(It.isAny(), It.isAny()),
Times.once()
);
});
});
context(
`if the answer to the message is 'Install Manually'`,
() => {
beforeEach(() => {
messageService
.setup((m) =>
m.info(
It.isAnyString(),
It.isAnyString(),
It.isAnyString()
)
)
.returns(() => Promise.resolve('Install Manually'));
});
it('should open the boards manager widget', () => {
subject.onStart();
});
}
);
});
context('if a candidate package for the board is not found', () => {
beforeEach(() => {
boardsService
.setup((b) => b.search(It.isValue({})))
.returns(async () => []);
});
it('should do nothing', async () => {
subject.onStart();
await tick();
messageService.verify(
(m) =>
m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()),
Times.never()
);
});
});
});
context(
'if one of the packages for the board is already installed',
() => {
beforeEach(() => {
boardsService
.setup((b) => b.search(It.isValue({})))
.returns(async () => [aPackage, anInstalledPackage]);
messageService
.setup((m) =>
m.info(It.isAnyString(), It.isAnyString(), It.isAnyString())
)
.returns(() => Promise.resolve('Yes'));
});
it('should do nothing', async () => {
subject.onStart();
await tick();
messageService.verify(
(m) =>
m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()),
Times.never()
);
});
}
);
});
context('and there is no selected board or port', () => {
it('should do nothing', async () => {
subject.onStart();
await tick();
messageService.verify(
(m) => m.info(It.isAnyString(), It.isAnyString(), It.isAnyString()),
Times.never()
);
});
});
});
});
});

View File

@@ -0,0 +1,3 @@
export function tick(): Promise<void> {
return new Promise((res) => setTimeout(res, 1));
}