diff --git a/src/app/index.ts b/src/app/index.ts index 927afe2..b27ac67 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -20,6 +20,7 @@ import Base = require('yeoman-generator'); enum ExtensionType { HelloWorld = 'hello-world', Widget = 'widget', + WidgetWithUnitTests = 'widget-with-unittest', LabelProvider = 'labelprovider', TreeEditor = 'tree-editor', Empty = 'empty', @@ -47,6 +48,10 @@ module.exports = class TheiaExtension extends Base { standalone: boolean dependencies: string browserDevDependencies: string + devdependencies: string + scripts: string + rootscripts: string + containsTests: boolean }; constructor(args: string | string[], options: any) { @@ -149,6 +154,7 @@ module.exports = class TheiaExtension extends Base { choices: [ { value: ExtensionType.HelloWorld, name: 'Hello World' }, { value: ExtensionType.Widget, name: 'Widget' }, + { value: ExtensionType.WidgetWithUnitTests, name: 'Widget with Unit Tests' }, { value: ExtensionType.LabelProvider, name: 'LabelProvider' }, { value: ExtensionType.TreeEditor, name: 'TreeEditor' }, { value: ExtensionType.Backend, name: 'Backend Communication' }, @@ -193,12 +199,17 @@ module.exports = class TheiaExtension extends Base { lernaVersion: options["lerna-version"], backend: options["extensionType"] === ExtensionType.Backend } + this.params.dependencies = ''; + this.params.browserDevDependencies = ''; if (this.params.extensionType === ExtensionType.TreeEditor) { this.params.dependencies = `,\n "@theia/editor": "${this.params.theiaVersion}",\n "@theia/filesystem": "${this.params.theiaVersion}",\n "@theia/workspace": "${this.params.theiaVersion}",\n "@eclipse-emfcloud/theia-tree-editor": "latest",\n "uuid": "^3.3.2"`; this.params.browserDevDependencies = `,\n "node-polyfill-webpack-plugin": "latest"`; - } else { - this.params.dependencies = ''; - this.params.browserDevDependencies = ''; + } + if (this.params.extensionType === ExtensionType.WidgetWithUnitTests) { + this.params.devdependencies = `,\n "@testing-library/react": "^11.2.7",\n "@types/jest": "^26.0.20",\n "jest": "^26.6.3",\n "ts-node": "^9.1.1",\n "ts-jest": "^26.5.6"`; + this.params.scripts = `,\n "test": "jest --config configs/jest.config.ts"`; + this.params.rootscripts =`,\n "test": "cd ${this.params.extensionPath} && yarn test"`; + this.params.containsTests = true; } options.params = this.params if (!options.standalone) { @@ -294,7 +305,7 @@ module.exports = class TheiaExtension extends Base { } /** widget */ - if (this.params.extensionType === ExtensionType.Widget) { + if (this.params.extensionType === ExtensionType.Widget||this.params.extensionType === ExtensionType.WidgetWithUnitTests) { this.fs.copyTpl( this.templatePath('widget/frontend-module.ts'), this.extensionPath(`src/browser/${this.params.extensionPath}-frontend-module.ts`), @@ -321,6 +332,24 @@ module.exports = class TheiaExtension extends Base { { params: this.params } ); } + /** widget with unit test */ + if (this.params.extensionType === ExtensionType.WidgetWithUnitTests) { + this.fs.copyTpl( + this.templatePath('widget/__tests__/widget.test.ts'), + this.extensionPath(`src/browser/__tests__/${this.params.extensionPath}-widget.test.ts`), + { params: this.params } + ); + this.fs.copyTpl( + this.templatePath('widget/__tests__/mock-objects/mock-message-service.ts'), + this.extensionPath(`src/browser/__tests__/mock-objects/mock-message-service.ts`), + { params: this.params } + ); + this.fs.copyTpl( + this.templatePath('widget/configs/jest.config.ts'), + this.extensionPath(`configs/jest.config.ts`), + { params: this.params } + ); + } /** backend */ if (this.params.extensionType === ExtensionType.Backend) { diff --git a/templates/README.md b/templates/README.md index cad29df..64e0e4e 100644 --- a/templates/README.md +++ b/templates/README.md @@ -41,6 +41,18 @@ Open http://localhost:3000 in the browser. yarn start *or:* launch `Start Electron Backend` configuration from VS code. + +<%if(params.containsTests){%> +## Running the tests + + yarn test + +*or* run the tests of a specific package with + + cd <%= params.extensionPath %> + yarn test + +<%}%> ## Developing with the browser example Start watching all packages, including `browser-app`, of your application with diff --git a/templates/extension-package.json b/templates/extension-package.json index 8f68f13..ec007e8 100644 --- a/templates/extension-package.json +++ b/templates/extension-package.json @@ -31,13 +31,13 @@ }, "devDependencies": { "rimraf": "latest", - "typescript": "latest" + "typescript": "latest"<% if (params.devdependencies) { %><%- params.devdependencies %><% } %> }, "scripts": { "prepare": "yarn run clean && yarn run build", "clean": "rimraf lib", "build": "tsc", - "watch": "tsc -w" + "watch": "tsc -w"<% if (params.scripts) { %><%- params.scripts %><% } %> }, "theiaExtensions": [ { diff --git a/templates/root-package.json b/templates/root-package.json index bfe1a62..55e3961 100644 --- a/templates/root-package.json +++ b/templates/root-package.json @@ -6,7 +6,7 @@ "rebuild:electron": "theia rebuild:electron", "start:browser": "yarn rebuild:browser && yarn --cwd browser-app start", "start:electron": "yarn rebuild:electron && yarn --cwd electron-app start", - "watch": "lerna run --parallel watch" + "watch": "lerna run --parallel watch"<% if (params.rootscripts) { %><%- params.rootscripts %><% } %> }, "devDependencies": { "lerna": "<%= params.lernaVersion %>" diff --git a/templates/widget/__tests__/mock-objects/mock-message-service.ts b/templates/widget/__tests__/mock-objects/mock-message-service.ts new file mode 100644 index 0000000..e9de7ec --- /dev/null +++ b/templates/widget/__tests__/mock-objects/mock-message-service.ts @@ -0,0 +1,64 @@ +import { injectable, inject } from 'inversify'; +import { Message, MessageClient, MessageOptions, MessageType, ProgressMessage, ProgressUpdate } from '@theia/core/lib/common/message-service-protocol'; +import { CancellationToken, MessageService } from '@theia/core'; + +@injectable() +export class MockMessageService extends MessageService{ + + constructor( + @inject(MessageClient) protected readonly client: MessageClient + ) { super(client) } + + logWasCalled: boolean = false; + infoWasCalled: boolean = false; + warnWasCalled: boolean = false; + errorWasCalled: boolean = false; + + log(message: string, ...actions: T[]): Promise; + log(message: string, options?: MessageOptions, ...actions: T[]): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + log(message: string, ...args: any[]): Promise { + this.logWasCalled = true; + return this.processMessage(MessageType.Log, message, args); + } + + info(message: string, ...actions: T[]): Promise; + + info(message: string, options?: MessageOptions, ...actions: T[]): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + info(message: string, ...args: any[]): Promise { + this.infoWasCalled = true; + return this.processMessage(MessageType.Info, message, args); + } + + warn(message: string, ...actions: T[]): Promise; + warn(message: string, options?: MessageOptions, ...actions: T[]): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + warn(message: string, ...args: any[]): Promise { + this.warnWasCalled = true; + return this.processMessage(MessageType.Warning, message, args); + } + + error(message: string, ...actions: T[]): Promise; + error(message: string, options?: MessageOptions, ...actions: T[]): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error(message: string, ...args: any[]): Promise { + this.errorWasCalled = true; + return this.processMessage(MessageType.Error, message, args); + } +} + +@injectable() +export class MockMessageClient { + + showMessage(message: Message): Promise { + return Promise.resolve(undefined); + } + + showProgress(progressId: string, message: ProgressMessage, cancellationToken: CancellationToken): Promise { + return Promise.resolve(undefined); + } + reportProgress(progressId: string, update: ProgressUpdate, message: ProgressMessage, cancellationToken: CancellationToken): Promise { + return Promise.resolve(undefined); + } +} diff --git a/templates/widget/__tests__/widget.test.ts b/templates/widget/__tests__/widget.test.ts new file mode 100644 index 0000000..177e3db --- /dev/null +++ b/templates/widget/__tests__/widget.test.ts @@ -0,0 +1,33 @@ +import 'reflect-metadata'; +import { MessageClient, MessageService } from '@theia/core'; +import { ContainerModule, Container } from 'inversify'; +import { MockMessageClient, MockMessageService } from './mock-objects/mock-message-service'; +import { <%= params.extensionPrefix %>Widget } from '../<%= params.extensionPath %>-widget'; +import { render } from '@testing-library/react' + +describe('widget extension unit tests', () => { + + let widget: any; + + beforeEach(async () => { + const module = new ContainerModule( bind => { + bind(MessageClient).to(MockMessageClient).inSingletonScope(); + bind(MessageService).to(MockMessageService).inSingletonScope(); + bind(<%= params.extensionPrefix %>Widget).toSelf(); + }); + const container = new Container(); + container.load(module); + widget = container.resolve<<%= params.extensionPrefix %>Widget>(<%= params.extensionPrefix %>Widget); + }); + + it('should render react node correctly', async () => { + const element = render(widget.render()); + expect(element.queryByText('Display Message')).toBeTruthy(); + }); + + it('should inject the message service', async () => { + widget.displayMessage(); + expect(widget.messageService.infoWasCalled).toBe(true); + }); + +}); \ No newline at end of file diff --git a/templates/widget/configs/jest.config.ts b/templates/widget/configs/jest.config.ts new file mode 100644 index 0000000..6641045 --- /dev/null +++ b/templates/widget/configs/jest.config.ts @@ -0,0 +1,10 @@ +import type { Config } from '@jest/types'; + +export default async (): Promise => ({ + preset: 'ts-jest', + testMatch: ['**.test.ts'], + rootDir: '../', + transform: { + '^.+\\.(ts)$': 'ts-jest', + } +}); \ No newline at end of file