diff --git a/README.md b/README.md index 688387b5..415bb199 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,11 @@ - Desktop application is created using [Electron](https://www.electronjs.org/). - Event driven as in components simply listens to events from the state and act accordingly. - **Application Layer** - - Keeps the application state - - The [state](src/application/Context/State/CategoryCollectionState.ts) is a mutable singleton & event producer. - - The application is defined & controlled in a [single YAML file](src/application/application.yaml) (see [Data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming)) + - Keeps the application state using [state pattern](https://en.wikipedia.org/wiki/State_pattern) + - [ApplicationContext](src/application/Context/ApplicationContext.ts) + - Holds the [CategoryCollectionState](src/application/Context/State/CategoryCollectionState.ts)] for each OS + - Same instance is shared throughout the application + - The application is defined & controlled in a [single YAML file](src/application/application.yaml) using[data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) ![DDD + vue.js](img/architecture/app-ddd.png) diff --git a/src/application/Context/ApplicationContext.ts b/src/application/Context/ApplicationContext.ts index da277938..9b15acfa 100644 --- a/src/application/Context/ApplicationContext.ts +++ b/src/application/Context/ApplicationContext.ts @@ -1,20 +1,72 @@ -import { IApplicationContext } from './IApplicationContext'; +import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext'; import { ICategoryCollectionState } from './State/ICategoryCollectionState'; import { CategoryCollectionState } from './State/CategoryCollectionState'; -import applicationFile from 'js-yaml-loader!@/application/application.yaml'; -import { parseCategoryCollection } from '../Parser/CategoryCollectionParser'; +import { IApplication } from '@/domain/IApplication'; +import { OperatingSystem } from '@/domain/OperatingSystem'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { Signal } from '@/infrastructure/Events/Signal'; - -export function createContext(): IApplicationContext { - const application = parseCategoryCollection(applicationFile); - const context = new ApplicationContext(application); - return context; -} +type StateMachine = Map; export class ApplicationContext implements IApplicationContext { - public readonly state: ICategoryCollectionState; - public constructor(public readonly collection: ICategoryCollection) { - this.state = new CategoryCollectionState(collection); + public readonly contextChanged = new Signal(); + public collection: ICategoryCollection; + public currentOs: OperatingSystem; + + public get state(): ICategoryCollectionState { + return this.states[this.collection.os]; + } + + private readonly states: StateMachine; + public constructor( + public readonly app: IApplication, + initialContext: OperatingSystem) { + validateApp(app); + validateOs(initialContext); + this.states = initializeStates(app); + this.changeContext(initialContext); + } + + public changeContext(os: OperatingSystem): void { + if (this.currentOs === os) { + return; + } + this.collection = this.app.getCollection(os); + if (!this.collection) { + throw new Error(`os "${OperatingSystem[os]}" is not defined in application`); + } + const event: IApplicationContextChangedEvent = { + newState: this.state, + newCollection: this.collection, + newOs: os, + }; + this.contextChanged.notify(event); + this.currentOs = os; } } + +function validateApp(app: IApplication) { + if (!app) { + throw new Error('undefined app'); + } +} + +function validateOs(os: OperatingSystem) { + if (os === undefined) { + throw new Error('undefined os'); + } + if (os === OperatingSystem.Unknown) { + throw new Error('unknown os'); + } + if (!(os in OperatingSystem)) { + throw new Error(`os "${os}" is out of range`); + } +} + +function initializeStates(app: IApplication): StateMachine { + const machine = new Map(); + for (const collection of app.collections) { + machine[collection.os] = new CategoryCollectionState(collection); + } + return machine; +} diff --git a/src/application/Context/ApplicationContextProvider.ts b/src/application/Context/ApplicationContextProvider.ts index 6f2b9abc..05f91eec 100644 --- a/src/application/Context/ApplicationContextProvider.ts +++ b/src/application/Context/ApplicationContextProvider.ts @@ -1,9 +1,32 @@ import { ApplicationContext } from './ApplicationContext'; import { IApplicationContext } from '@/application/Context/IApplicationContext'; -import applicationFile from 'js-yaml-loader!@/application/application.yaml'; -import { parseCategoryCollection } from '@/application/Parser/CategoryCollectionParser'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { Environment } from '../Environment/Environment'; +import { IApplication } from '@/domain/IApplication'; +import { IEnvironment } from '../Environment/IEnvironment'; +import { parseApplication } from '../Parser/ApplicationParser'; -export function buildContext(): IApplicationContext { - const application = parseCategoryCollection(applicationFile); - return new ApplicationContext(application); +export type ApplicationParserType = () => IApplication; +const ApplicationParser: ApplicationParserType = parseApplication; + +export function buildContext( + parser = ApplicationParser, + environment = Environment.CurrentEnvironment): IApplicationContext { + const app = parser(); + const os = getInitialOs(app, environment); + return new ApplicationContext(app, os); +} + +function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem { + const currentOs = environment.os; + const supportedOsList = app.getSupportedOsList(); + if (supportedOsList.includes(currentOs)) { + return currentOs; + } + supportedOsList.sort((os1, os2) => { + const os1SupportLevel = app.collections[os1].totalScripts; + const os2SupportLevel = app.collections[os2].totalScripts; + return os1SupportLevel - os2SupportLevel; + }); + return supportedOsList[0]; } diff --git a/src/application/Context/IApplicationContext.ts b/src/application/Context/IApplicationContext.ts index 9aac8594..24a6d66b 100644 --- a/src/application/Context/IApplicationContext.ts +++ b/src/application/Context/IApplicationContext.ts @@ -1,7 +1,20 @@ import { ICategoryCollectionState } from './State/ICategoryCollectionState'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { ISignal } from '@/infrastructure/Events/ISignal'; +import { IApplication } from '@/domain/IApplication'; export interface IApplicationContext { + readonly currentOs: OperatingSystem; + readonly app: IApplication; readonly collection: ICategoryCollection; readonly state: ICategoryCollectionState; + readonly contextChanged: ISignal; + changeContext(os: OperatingSystem): void; +} + +export interface IApplicationContextChangedEvent { + readonly newState: ICategoryCollectionState; + readonly newCollection: ICategoryCollection; + readonly newOs: OperatingSystem; } diff --git a/src/application/Environment/IEnvironment.ts b/src/application/Environment/IEnvironment.ts index 14e53ca6..e2128e2e 100644 --- a/src/application/Environment/IEnvironment.ts +++ b/src/application/Environment/IEnvironment.ts @@ -1,6 +1,6 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; export interface IEnvironment { - isDesktop: boolean; - os: OperatingSystem; + readonly isDesktop: boolean; + readonly os: OperatingSystem; } diff --git a/src/application/Parser/ApplicationParser.ts b/src/application/Parser/ApplicationParser.ts new file mode 100644 index 00000000..bd85c068 --- /dev/null +++ b/src/application/Parser/ApplicationParser.ts @@ -0,0 +1,27 @@ +import { IApplication } from '@/domain/IApplication'; +import { IProjectInformation } from '@/domain/IProjectInformation'; +import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { parseCategoryCollection } from './CategoryCollectionParser'; +import applicationFile, { YamlApplication } from 'js-yaml-loader!@/application/application.yaml'; +import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; +import { Application } from '@/domain/Application'; + +export function parseApplication( + parser = CategoryCollectionParser, + processEnv: NodeJS.ProcessEnv = process.env, + collectionData = CollectionData): IApplication { + const information = parseProjectInformation(processEnv); + const collection = parser(collectionData, information); + const app = new Application(information, [ collection ]); + return app; +} + +export type CategoryCollectionParserType + = (file: YamlApplication, info: IProjectInformation) => ICategoryCollection; + +const CategoryCollectionParser: CategoryCollectionParserType + = (file, info) => parseCategoryCollection(file, info); + +const CollectionData: YamlApplication + = applicationFile; + diff --git a/src/application/Parser/CategoryCollectionParser.ts b/src/application/Parser/CategoryCollectionParser.ts index 0231bb5f..7270c9c7 100644 --- a/src/application/Parser/CategoryCollectionParser.ts +++ b/src/application/Parser/CategoryCollectionParser.ts @@ -1,17 +1,17 @@ import { Category } from '@/domain/Category'; import { YamlApplication } from 'js-yaml-loader!@/application.yaml'; import { parseCategory } from './CategoryParser'; -import { parseProjectInformation } from './ProjectInformationParser'; import { ScriptCompiler } from './Compiler/ScriptCompiler'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { parseScriptingDefinition } from './ScriptingDefinitionParser'; import { createEnumParser } from '../Common/Enum'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { CategoryCollection } from '@/domain/CategoryCollection'; +import { IProjectInformation } from '@/domain/IProjectInformation'; export function parseCategoryCollection( content: YamlApplication, - env: NodeJS.ProcessEnv = process.env, + info: IProjectInformation, osParser = createEnumParser(OperatingSystem)): ICategoryCollection { validate(content); const compiler = new ScriptCompiler(content.functions); @@ -21,11 +21,9 @@ export function parseCategoryCollection( categories.push(category); } const os = osParser.parseEnum(content.os, 'os'); - const info = parseProjectInformation(env); const scripting = parseScriptingDefinition(content.scripting, info); const collection = new CategoryCollection( os, - info, categories, scripting); return collection; diff --git a/src/domain/Application.ts b/src/domain/Application.ts new file mode 100644 index 00000000..3b5d70d3 --- /dev/null +++ b/src/domain/Application.ts @@ -0,0 +1,47 @@ +import { IApplication } from './IApplication'; +import { ICategoryCollection } from './ICategoryCollection'; +import { IProjectInformation } from './IProjectInformation'; +import { OperatingSystem } from './OperatingSystem'; + +export class Application implements IApplication { + constructor(public info: IProjectInformation, public collections: readonly ICategoryCollection[]) { + validateInformation(info); + validateCollections(collections); + } + + public getSupportedOsList(): OperatingSystem[] { + return this.collections.map((collection) => collection.os); + } + + public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined { + return this.collections.find((collection) => collection.os === operatingSystem); + } +} + +function validateInformation(info: IProjectInformation) { + if (!info) { + throw new Error('undefined project information'); + } +} + +function validateCollections(collections: readonly ICategoryCollection[]) { + if (!collections) { + throw new Error('undefined collections'); + } + if (collections.length === 0) { + throw new Error('no collection in the list'); + } + if (collections.filter((c) => !c).length > 0) { + throw new Error('undefined collection in the list'); + } + const osList = collections.map((c) => c.os); + const duplicates = getDuplicates(osList); + if (duplicates.length > 0) { + throw new Error('multiple collections with same os: ' + + duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "')); + } +} + +function getDuplicates(list: readonly OperatingSystem[]): OperatingSystem[] { + return list.filter((os, index) => list.indexOf(os) !== index); +} diff --git a/src/domain/CategoryCollection.ts b/src/domain/CategoryCollection.ts index 6678e092..29b69dc3 100644 --- a/src/domain/CategoryCollection.ts +++ b/src/domain/CategoryCollection.ts @@ -2,7 +2,6 @@ import { getEnumNames, getEnumValues } from '@/application/Common/Enum'; import { IEntity } from '../infrastructure/Entity/IEntity'; import { ICategory } from './ICategory'; import { IScript } from './IScript'; -import { IProjectInformation } from './IProjectInformation'; import { RecommendationLevel } from './RecommendationLevel'; import { OperatingSystem } from './OperatingSystem'; import { IScriptingDefinition } from './IScriptingDefinition'; @@ -16,12 +15,8 @@ export class CategoryCollection implements ICategoryCollection { constructor( public readonly os: OperatingSystem, - public readonly info: IProjectInformation, public readonly actions: ReadonlyArray, public readonly scripting: IScriptingDefinition) { - if (!info) { - throw new Error('undefined info'); - } if (!scripting) { throw new Error('undefined scripting definition'); } diff --git a/src/domain/IApplication.ts b/src/domain/IApplication.ts new file mode 100644 index 00000000..d456d88e --- /dev/null +++ b/src/domain/IApplication.ts @@ -0,0 +1,11 @@ +import { ICategoryCollection } from './ICategoryCollection'; +import { IProjectInformation } from './IProjectInformation'; +import { OperatingSystem } from './OperatingSystem'; + +export interface IApplication { + readonly info: IProjectInformation; + readonly collections: readonly ICategoryCollection[]; + + getSupportedOsList(): OperatingSystem[]; + getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined; +} diff --git a/src/domain/ICategoryCollection.ts b/src/domain/ICategoryCollection.ts index 25bf3514..39817403 100644 --- a/src/domain/ICategoryCollection.ts +++ b/src/domain/ICategoryCollection.ts @@ -3,10 +3,8 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; import { RecommendationLevel } from '@/domain/RecommendationLevel'; import { IScript } from '@/domain/IScript'; import { ICategory } from '@/domain/ICategory'; -import { IProjectInformation } from '@/domain/IProjectInformation'; export interface ICategoryCollection { - readonly info: IProjectInformation; readonly scripting: IScriptingDefinition; readonly os: OperatingSystem; readonly totalScripts: number; diff --git a/src/presentation/Scripts/TheScripts.vue b/src/presentation/Scripts/TheScripts.vue index c50994a9..8795fc37 100644 --- a/src/presentation/Scripts/TheScripts.vue +++ b/src/presentation/Scripts/TheScripts.vue @@ -74,7 +74,7 @@ public async mounted() { const context = await this.getCurrentContextAsync(); - this.repositoryUrl = context.collection.info.repositoryWebUrl; + this.repositoryUrl = context.app.info.repositoryWebUrl; const filter = context.state.filter; filter.filterRemoved.on(() => { this.isSearching = false; diff --git a/src/presentation/TheFooter/DownloadUrlListItem.vue b/src/presentation/TheFooter/DownloadUrlListItem.vue index 7eb192bd..dc876d2c 100644 --- a/src/presentation/TheFooter/DownloadUrlListItem.vue +++ b/src/presentation/TheFooter/DownloadUrlListItem.vue @@ -39,7 +39,7 @@ export default class DownloadUrlListItem extends StatefulVue { private async getDownloadUrlAsync(os: OperatingSystem): Promise { const context = await this.getCurrentContextAsync(); - return context.collection.info.getDownloadUrl(os); + return context.app.info.getDownloadUrl(os); } } diff --git a/src/presentation/TheFooter/PrivacyPolicy.vue b/src/presentation/TheFooter/PrivacyPolicy.vue index 3e160d18..087887c2 100644 --- a/src/presentation/TheFooter/PrivacyPolicy.vue +++ b/src/presentation/TheFooter/PrivacyPolicy.vue @@ -48,8 +48,9 @@ export default class TheFooter extends StatefulVue { public async mounted() { const context = await this.getCurrentContextAsync(); - this.repositoryUrl = context.collection.info.repositoryWebUrl; - this.feedbackUrl = context.collection.info.feedbackUrl; + const info = context.app.info; + this.repositoryUrl = info.repositoryWebUrl; + this.feedbackUrl = info.feedbackUrl; } } diff --git a/src/presentation/TheFooter/TheFooter.vue b/src/presentation/TheFooter/TheFooter.vue index 74580ab4..0b7e3581 100644 --- a/src/presentation/TheFooter/TheFooter.vue +++ b/src/presentation/TheFooter/TheFooter.vue @@ -75,7 +75,7 @@ export default class TheFooter extends StatefulVue { public async mounted() { const context = await this.getCurrentContextAsync(); - const info = context.collection.info; + const info = context.app.info; this.version = info.version; this.homepageUrl = info.homepage; this.repositoryUrl = info.repositoryWebUrl; diff --git a/src/presentation/TheHeader.vue b/src/presentation/TheHeader.vue index 659d269b..5c3be536 100644 --- a/src/presentation/TheHeader.vue +++ b/src/presentation/TheHeader.vue @@ -16,7 +16,7 @@ export default class TheHeader extends StatefulVue { public async mounted() { const context = await this.getCurrentContextAsync(); - this.title = context.collection.info.name; + this.title = context.app.info.name; } } diff --git a/tests/unit/application/Context/ApplicationContext.spec.ts b/tests/unit/application/Context/ApplicationContext.spec.ts new file mode 100644 index 00000000..8934b9f0 --- /dev/null +++ b/tests/unit/application/Context/ApplicationContext.spec.ts @@ -0,0 +1,272 @@ +import 'mocha'; +import { expect } from 'chai'; +import { ApplicationContext } from '@/application/Context/ApplicationContext'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { IApplicationContext, IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext'; +import { IApplication } from '@/domain/IApplication'; +import { ApplicationStub } from '../../stubs/ApplicationStub'; +import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub'; + +describe('ApplicationContext', () => { + describe('changeContext', () => { + describe('when initial os is changed to different one', () => { + it('collection is changed as expected', () => { + // arrange + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(OperatingSystem.Windows, OperatingSystem.macOS); + const expectedCollection = testContext.app.getCollection(OperatingSystem.macOS); + // act + const sut = testContext + .withInitialOs(OperatingSystem.Windows) + .construct(); + sut.changeContext(OperatingSystem.macOS); + // assert + expect(sut.collection).to.equal(expectedCollection); + }); + it('currentOs is changed as expected', () => { + // arrange + const expectedOs = OperatingSystem.macOS; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(OperatingSystem.Windows, expectedOs); + // act + const sut = testContext + .withInitialOs(OperatingSystem.Windows) + .construct(); + sut.changeContext(expectedOs); + // assert + expect(sut.currentOs).to.equal(expectedOs); + }); + it('state is changed as expected', () => { + // arrange + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(OperatingSystem.Windows, OperatingSystem.macOS); + // act + const sut = testContext + .withInitialOs(OperatingSystem.Windows) + .construct(); + sut.changeContext(OperatingSystem.macOS); + // assert + expectEmptyState(sut.state); + }); + }); + it('remembers old state when changed backed to same os', () => { + // arrange + const os = OperatingSystem.Windows; + const changedOs = OperatingSystem.macOS; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(os, changedOs); + const expectedFilter = 'first-state'; + // act + const sut = testContext + .withInitialOs(os) + .construct(); + const firstState = sut.state; + firstState.filter.setFilter(expectedFilter); + sut.changeContext(os); + sut.changeContext(changedOs); + sut.state.filter.setFilter('second-state'); + sut.changeContext(os); + // assert + const actualFilter = sut.state.filter.currentFilter.query; + expect(actualFilter).to.equal(expectedFilter); + }); + describe('contextChanged', () => { + it('fired as expected on change', () => { + // arrange + const nextOs = OperatingSystem.macOS; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(OperatingSystem.Windows, nextOs); + const expectedCollection = testContext.app.getCollection(OperatingSystem.macOS); + // act + const sut = testContext + .withInitialOs(OperatingSystem.Windows) + .construct(); + sut.changeContext(nextOs); + // assert + expect(testContext.firedEvents.length).to.equal(1); + expect(testContext.firedEvents[0].newCollection).to.equal(expectedCollection); + expect(testContext.firedEvents[0].newState).to.equal(sut.state); + expect(testContext.firedEvents[0].newOs).to.equal(nextOs); + }); + it('is not fired when initial os is changed to same one', () => { + // arrange + const os = OperatingSystem.Windows; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(os); + // act + const sut = testContext + .withInitialOs(os) + .construct(); + const initialState = sut.state; + initialState.filter.setFilter('dirty-state'); + sut.changeContext(os); + // assert + expect(testContext.firedEvents.length).to.equal(0); + }); + it('new event is fired for each change', () => { + // arrange + const os = OperatingSystem.Windows; + const changedOs = OperatingSystem.macOS; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(os, changedOs); + // act + const sut = testContext + .withInitialOs(os) + .construct(); + sut.changeContext(changedOs); + sut.changeContext(os); + sut.changeContext(changedOs); + // assert + const duplicates = getDuplicates(testContext.firedEvents); + expect(duplicates.length).to.be.equal(0); + }); + }); + }); + describe('ctor', () => { + describe('app', () => { + it('throw when app is undefined', () => { + // arrange + const expectedError = 'undefined app'; + const app = undefined; + const os = OperatingSystem.Windows; + // act + const act = () => new ApplicationContext(app, os); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('collection', () => { + it('returns right collection for expected OS', () => { + // arrange + const os = OperatingSystem.Windows; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(os); + const expected = testContext.app.getCollection(os); + // act + const sut = testContext + .withInitialOs(os) + .construct(); + // assert + const actual = sut.collection; + expect(actual).to.deep.equal(expected); + }); + }); + describe('state', () => { + it('initially returns an empty state', () => { + // arrange + const sut = new ObservableApplicationContextFactory() + .construct(); + // act + const actual = sut.state; + // assert + expectEmptyState(actual); + }); + }); + describe('os', () => { + it('set as initial OS', () => { + // arrange + const expected = OperatingSystem.Windows; + const testContext = new ObservableApplicationContextFactory() + .withAppContainingCollections(OperatingSystem.macOS, expected); + // act + const sut = testContext + .withInitialOs(expected) + .construct(); + // assert + const actual = sut.currentOs; + expect(actual).to.deep.equal(expected); + }); + describe('throws when OS is invalid', () => { + // arrange + const testCases = [ + { + name: 'out of range', + expectedError: 'os "9999" is out of range', + os: 9999, + }, + { + name: 'undefined', + expectedError: 'undefined os', + os: undefined, + }, + { + name: 'unknown', + expectedError: 'unknown os', + os: OperatingSystem.Unknown, + }, + { + name: 'does not exist in application', + expectedError: 'os "Android" is not defined in application', + os: OperatingSystem.Android, + }, + ]; + // act + for (const testCase of testCases) { + it(testCase.name, () => { + const act = () => + new ObservableApplicationContextFactory() + .withInitialOs(testCase.os) + .construct(); + // assert + expect(act).to.throw(testCase.expectedError); + }); + } + }); + }); + describe('app', () => { + it('sets app as expected', () => { + // arrange + const os = OperatingSystem.Windows; + const expected = new ApplicationStub().withCollection( + new CategoryCollectionStub().withOs(os), + ); + // act + const sut = new ObservableApplicationContextFactory() + .withApp(expected) + .withInitialOs(os) + .construct(); + // assert + expect(expected).to.equal(sut.app); + }); + }); + }); +}); + +class ObservableApplicationContextFactory { + private static DefaultOs = OperatingSystem.Windows; + public app: IApplication; + public firedEvents = new Array(); + private initialOs = ObservableApplicationContextFactory.DefaultOs; + constructor() { + this.withAppContainingCollections(ObservableApplicationContextFactory.DefaultOs); + } + public withAppContainingCollections(...oses: OperatingSystem[]): ObservableApplicationContextFactory { + const collectionValues = oses.map((os) => new CategoryCollectionStub().withOs(os)); + const app = new ApplicationStub().withCollections(...collectionValues); + return this.withApp(app); + } + public withApp(app: IApplication): ObservableApplicationContextFactory { + this.app = app; + return this; + } + public withInitialOs(initialOs: OperatingSystem) { + this.initialOs = initialOs; + return this; + } + public construct() + : IApplicationContext { + const sut = new ApplicationContext(this.app, this.initialOs); + sut.contextChanged.on((newContext) => this.firedEvents.push(newContext)); + return sut; + } +} +function getDuplicates(list: readonly T[]): T[] { + return list.filter((item, index) => list.indexOf(item) !== index); +} + +function expectEmptyState(state: ICategoryCollectionState) { + expect(!state.code.current); + expect(!state.filter.currentFilter); + expect(!state.selection); +} diff --git a/tests/unit/application/Context/ApplicationContextProvider.spec.ts b/tests/unit/application/Context/ApplicationContextProvider.spec.ts new file mode 100644 index 00000000..894a66e6 --- /dev/null +++ b/tests/unit/application/Context/ApplicationContextProvider.spec.ts @@ -0,0 +1,54 @@ +import 'mocha'; +import { expect } from 'chai'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { ApplicationParserType, buildContext } from '@/application/Context/ApplicationContextProvider'; +import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub'; +import { EnvironmentStub } from '../../stubs/EnvironmentStub'; +import { ApplicationStub } from '../../stubs/ApplicationStub'; + +describe('ApplicationContextProvider', () => { + describe('buildContext', () => { + it('sets application from parser', () => { + // arrange + const expected = new ApplicationStub().withCollection( + new CategoryCollectionStub().withOs(OperatingSystem.macOS)); + const parserMock: ApplicationParserType = () => expected; + // act + const context = buildContext(parserMock); + // assert + // TODO: expect(expected).to.equal(context.app); + }); + describe('sets initial OS as expected', () => { + it('returns currentOs if it is supported', () => { + // arrange + const expected = OperatingSystem.Windows; + const environment = new EnvironmentStub().withOs(expected); + const parser = mockParser(new CategoryCollectionStub().withOs(expected)); + // act + const context = buildContext(parser, environment); + // assert + expect(expected).to.equal(context.currentOs); + }); + it('fallbacks to other os if OS in environment is not supported', () => { + // arrange + const expected = OperatingSystem.Windows; + const currentOs = OperatingSystem.macOS; + const environment = new EnvironmentStub().withOs(currentOs); + const parser = mockParser(new CategoryCollectionStub().withOs(expected)); + // act + const context = buildContext(parser, environment); + // assert + const actual = context.currentOs; + expect(expected).to.equal(actual); + }); + it('fallbacks to most supported os if current os is not supported', () => { + // TODO: After more than single collection can be parsed + }); + }); + }); +}); + +function mockParser(result: ICategoryCollection): ApplicationParserType { + return () => new ApplicationStub().withCollection(result); +} diff --git a/tests/unit/application/Context/State/ApplicationContext.spec.ts b/tests/unit/application/Context/State/ApplicationContext.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts b/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts index f614fec1..961df803 100644 --- a/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts +++ b/tests/unit/application/Context/State/Code/ApplicationCode.spec.ts @@ -14,8 +14,6 @@ import { CategoryStub } from '../../../../stubs/CategoryStub'; import { ScriptStub } from '../../../../stubs/ScriptStub'; import { CategoryCollectionStub } from '../../../../stubs/CategoryCollectionStub'; -// TODO: Test scriptingDefinition: IScriptingDefinition logic - describe('ApplicationCode', () => { describe('ctor', () => { it('empty when selection is empty', () => { diff --git a/tests/unit/application/Parser/ApplicationParser.spec.ts b/tests/unit/application/Parser/ApplicationParser.spec.ts new file mode 100644 index 00000000..144a7216 --- /dev/null +++ b/tests/unit/application/Parser/ApplicationParser.spec.ts @@ -0,0 +1,108 @@ +import 'mocha'; +import { expect } from 'chai'; +import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; +import { CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser'; +import applicationFile, { YamlApplication } from 'js-yaml-loader!@/application/application.yaml'; +import { IProjectInformation } from '@/domain/IProjectInformation'; +import { ProjectInformation } from '@/domain/ProjectInformation'; +import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub'; +import { getProcessEnvironmentStub } from '../../stubs/ProcessEnvironmentStub'; +import { YamlApplicationStub } from '../../stubs/YamlApplicationStub'; + +describe('ApplicationParser', () => { + describe('parseApplication', () => { + it('can parse current application', () => { // Integration test + // act + const act = () => parseApplication(); + // assert + expect(act).to.not.throw(); + }); + describe('parser', () => { + it('returns result from the parser', () => { + // arrange + const os = OperatingSystem.macOS; + const expected = new CategoryCollectionStub() + .withOs(os); + const parser = new CategoryCollectionParserSpy() + .setResult(expected) + .mockParser(); + // act + const context = parseApplication(parser); + // assert + const actual = context.getCollection(os); + expect(expected).to.equal(actual); + }); + }); + describe('processEnv', () => { + it('used to parse expected project information', () => { + // arrange + const env = getProcessEnvironmentStub(); + const expected = parseProjectInformation(env); + const parserSpy = new CategoryCollectionParserSpy(); + const parserMock = parserSpy.mockParser(); + // act + const context = parseApplication(parserMock, env); + // assert + expect(expected).to.deep.equal(context.info); + expect(expected).to.deep.equal(parserSpy.lastArguments.info); + }); + it('defaults to process.env', () => { + // arrange + const env = process.env; + const expected = parseProjectInformation(env); + const parserSpy = new CategoryCollectionParserSpy(); + const parserMock = parserSpy.mockParser(); + // act + const context = parseApplication(parserMock); + // assert + expect(expected).to.deep.equal(context.info); + expect(expected).to.deep.equal(parserSpy.lastArguments.info); + }); + }); + describe('collectionData', () => { + it('parsed with expected data', () => { + // arrange + const expected = new YamlApplicationStub(); + const env = getProcessEnvironmentStub(); + const parserSpy = new CategoryCollectionParserSpy(); + const parserMock = parserSpy.mockParser(); + // act + parseApplication(parserMock, env, expected); + // assert + expect(expected).to.equal(parserSpy.lastArguments.file); + }); + it('defaults to applicationFile', () => { + // arrange + const expected = applicationFile; + const parserSpy = new CategoryCollectionParserSpy(); + const parserMock = parserSpy.mockParser(); + // act + parseApplication(parserMock); + // assert + expect(expected).to.equal(parserSpy.lastArguments.file); + }); + }); + }); +}); + +class CategoryCollectionParserSpy { + public lastArguments: { + file: YamlApplication; + info: ProjectInformation; + } = { file: undefined, info: undefined }; + private result: ICategoryCollection = new CategoryCollectionStub(); + + public setResult(collection: ICategoryCollection): CategoryCollectionParserSpy { + this.result = collection; + return this; + } + public mockParser(): CategoryCollectionParserType { + return (file: YamlApplication, info: IProjectInformation) => { + this.lastArguments.file = file; + this.lastArguments.info = info; + return this.result; + }; + } +} diff --git a/tests/unit/application/Parser/CategoryCollectionParser.spec.ts b/tests/unit/application/Parser/CategoryCollectionParser.spec.ts index cbca5fba..e2587719 100644 --- a/tests/unit/application/Parser/CategoryCollectionParser.spec.ts +++ b/tests/unit/application/Parser/CategoryCollectionParser.spec.ts @@ -1,30 +1,24 @@ -import { IEntity } from '@/infrastructure/Entity/IEntity'; -import applicationFile, { YamlCategory, YamlScript, YamlApplication, YamlScriptingDefinition } from 'js-yaml-loader!@/application/application.yaml'; -import { parseCategoryCollection } from '@/application/Parser/CategoryCollectionParser'; import 'mocha'; import { expect } from 'chai'; +import { IEntity } from '@/infrastructure/Entity/IEntity'; +import { parseCategoryCollection } from '@/application/Parser/CategoryCollectionParser'; import { parseCategory } from '@/application/Parser/CategoryParser'; -import { RecommendationLevel } from '@/domain/RecommendationLevel'; -import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub'; import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinitionParser'; import { mockEnumParser } from '../../stubs/EnumParserStub'; +import { ProjectInformationStub } from '../../stubs/ProjectInformationStub'; +import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub'; +import { getCategoryStub, YamlApplicationStub } from '../../stubs/YamlApplicationStub'; describe('CategoryCollectionParser', () => { describe('parseCategoryCollection', () => { - it('can parse current application file', () => { - // act - const act = () => parseCategoryCollection(applicationFile); - // assert - expect(act).to.not.throw(); - }); it('throws when undefined', () => { // arrange const expectedError = 'content is null or undefined'; + const info = new ProjectInformationStub(); // act - const act = () => parseCategoryCollection(undefined); + const act = () => parseCategoryCollection(undefined, info); // assert expect(act).to.throw(expectedError); }); @@ -32,29 +26,35 @@ describe('CategoryCollectionParser', () => { it('throws when undefined actions', () => { // arrange const expectedError = 'content does not define any action'; - const collection = new YamlApplicationBuilder().withActions(undefined).build(); + const collection = new YamlApplicationStub() + .withActions(undefined); + const info = new ProjectInformationStub(); // act - const act = () => parseCategoryCollection(collection); + const act = () => parseCategoryCollection(collection, info); // assert expect(act).to.throw(expectedError); }); it('throws when has no actions', () => { // arrange const expectedError = 'content does not define any action'; - const collection = new YamlApplicationBuilder().withActions([]).build(); + const collection = new YamlApplicationStub() + .withActions([]); + const info = new ProjectInformationStub(); // act - const act = () => parseCategoryCollection(collection); + const act = () => parseCategoryCollection(collection, info); // assert expect(act).to.throw(expectedError); }); it('parses actions', () => { // arrange - const actions = [ getTestCategory('test1'), getTestCategory('test2') ]; + const actions = [ getCategoryStub('test1'), getCategoryStub('test2') ]; const compiler = new ScriptCompilerStub(); const expected = [ parseCategory(actions[0], compiler), parseCategory(actions[1], compiler) ]; - const collection = new YamlApplicationBuilder().withActions(actions).build(); + const collection = new YamlApplicationStub() + .withActions(actions); + const info = new ProjectInformationStub(); // act - const actual = parseCategoryCollection(collection).actions; + const actual = parseCategoryCollection(collection, info).actions; // assert expect(excludingId(actual)).to.be.deep.equal(excludingId(expected)); function excludingId(array: ReadonlyArray>) { @@ -65,60 +65,14 @@ describe('CategoryCollectionParser', () => { } }); }); - describe('info', () => { - it('returns expected repository version', () => { - // arrange - const expected = 'expected-version'; - const env = getProcessEnvironmentStub(); - env.VUE_APP_VERSION = expected; - const collection = new YamlApplicationBuilder().build(); - // act - const actual = parseCategoryCollection(collection, env).info.version; - // assert - expect(actual).to.be.equal(expected); - }); - it('returns expected repository url', () => { - // arrange - const expected = 'https://expected-repository.url'; - const env = getProcessEnvironmentStub(); - env.VUE_APP_REPOSITORY_URL = expected; - const collection = new YamlApplicationBuilder().build(); - // act - const actual = parseCategoryCollection(collection, env).info.repositoryUrl; - // assert - expect(actual).to.be.equal(expected); - }); - it('returns expected name', () => { - // arrange - const expected = 'expected-app-name'; - const env = getProcessEnvironmentStub(); - env.VUE_APP_NAME = expected; - const collection = new YamlApplicationBuilder().build(); - // act - const actual = parseCategoryCollection(collection, env).info.name; - // assert - expect(actual).to.be.equal(expected); - }); - it('returns expected homepage url', () => { - // arrange - const expected = 'https://expected.sexy'; - const env = getProcessEnvironmentStub(); - env.VUE_APP_HOMEPAGE_URL = expected; - const collection = new YamlApplicationBuilder().build(); - // act - const actual = parseCategoryCollection(collection, env).info.homepage; - // assert - expect(actual).to.be.equal(expected); - }); - }); describe('scripting definition', () => { it('parses scripting definition as expected', () => { // arrange - const collection = new YamlApplicationBuilder().build(); + const collection = new YamlApplicationStub(); const information = parseProjectInformation(process.env); const expected = parseScriptingDefinition(collection.scripting, information); // act - const actual = parseCategoryCollection(collection).scripting; + const actual = parseCategoryCollection(collection, information).scripting; // assert expect(expected).to.deep.equal(actual); }); @@ -129,79 +83,15 @@ describe('CategoryCollectionParser', () => { const expectedOs = OperatingSystem.macOS; const osText = 'macos'; const expectedName = 'os'; - const collection = new YamlApplicationBuilder() - .withOs(osText) - .build(); + const collection = new YamlApplicationStub() + .withOs(osText); const parserMock = mockEnumParser(expectedName, osText, expectedOs); - const env = getProcessEnvironmentStub(); + const info = new ProjectInformationStub(); // act - const actual = parseCategoryCollection(collection, env, parserMock); + const actual = parseCategoryCollection(collection, info, parserMock); // assert expect(actual.os).to.equal(expectedOs); }); }); }); }); - -class YamlApplicationBuilder { - private os = 'windows'; - private actions: readonly YamlCategory[] = [ getTestCategory() ]; - private scripting: YamlScriptingDefinition = getTestDefinition(); - - public withActions(actions: readonly YamlCategory[]): YamlApplicationBuilder { - this.actions = actions; - return this; - } - - public withOs(os: string): YamlApplicationBuilder { - this.os = os; - return this; - } - - public withScripting(scripting: YamlScriptingDefinition): YamlApplicationBuilder { - this.scripting = scripting; - return this; - } - - public build(): YamlApplication { - return { os: this.os, scripting: this.scripting, actions: this.actions }; - } -} - -function getTestDefinition(): YamlScriptingDefinition { - return { - fileExtension: '.bat', - language: ScriptingLanguage[ScriptingLanguage.batchfile], - startCode: 'start', - endCode: 'end', - }; -} - -function getTestCategory(scriptPrefix = 'testScript'): YamlCategory { - return { - category: 'category name', - children: [ - getTestScript(`${scriptPrefix}-standard`, RecommendationLevel.Standard), - getTestScript(`${scriptPrefix}-strict`, RecommendationLevel.Strict), - ], - }; -} - -function getTestScript(scriptName: string, level: RecommendationLevel = RecommendationLevel.Standard): YamlScript { - return { - name: scriptName, - code: 'script code', - revertCode: 'revert code', - recommend: RecommendationLevel[level].toLowerCase(), - call: undefined, - }; -} - -function getProcessEnvironmentStub(): NodeJS.ProcessEnv { - return { - VUE_APP_VERSION: 'stub-version', - VUE_APP_NAME: 'stub-name', - VUE_APP_REPOSITORY_URL: 'stub-repository-url', - VUE_APP_HOMEPAGE_URL: 'stub-homepage-url', - }; -} diff --git a/tests/unit/application/Parser/ProjectInformationParser.spec.ts b/tests/unit/application/Parser/ProjectInformationParser.spec.ts index 845be924..dbe0de25 100644 --- a/tests/unit/application/Parser/ProjectInformationParser.spec.ts +++ b/tests/unit/application/Parser/ProjectInformationParser.spec.ts @@ -1,6 +1,7 @@ import 'mocha'; import { expect } from 'chai'; import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; +import { getProcessEnvironmentStub } from '../../stubs/ProcessEnvironmentStub'; describe('ProjectInformationParser', () => { describe('parseProjectInformation', () => { @@ -46,13 +47,3 @@ describe('ProjectInformationParser', () => { }); }); }); - - -function getProcessEnvironmentStub(): NodeJS.ProcessEnv { - return { - VUE_APP_VERSION: 'stub-version', - VUE_APP_NAME: 'stub-name', - VUE_APP_REPOSITORY_URL: 'stub-repository-url', - VUE_APP_HOMEPAGE_URL: 'stub-homepage-url', - }; -} diff --git a/tests/unit/domain/Application.spec.ts b/tests/unit/domain/Application.spec.ts new file mode 100644 index 00000000..863f2a68 --- /dev/null +++ b/tests/unit/domain/Application.spec.ts @@ -0,0 +1,118 @@ +import 'mocha'; +import { expect } from 'chai'; +import { Application } from '@/domain/Application'; +import { CategoryCollectionStub } from '../stubs/CategoryCollectionStub'; +import { ProjectInformationStub } from '../stubs/ProjectInformationStub'; +import { OperatingSystem } from '@/domain/OperatingSystem'; + +describe('Application', () => { + describe('getCollection', () => { + it('returns undefined if not found', () => { + // arrange + const expected = undefined; + const info = new ProjectInformationStub(); + const collections = [ new CategoryCollectionStub().withOs(OperatingSystem.Windows) ]; + // act + const sut = new Application(info, collections); + const actual = sut.getCollection(OperatingSystem.Android); + // assert + expect(actual).to.equals(expected); + }); + it('returns expected when multiple collections exist', () => { + // arrange + const os = OperatingSystem.Windows; + const expected = new CategoryCollectionStub().withOs(os); + const info = new ProjectInformationStub(); + const collections = [ expected, new CategoryCollectionStub().withOs(OperatingSystem.Android) ]; + // act + const sut = new Application(info, collections); + const actual = sut.getCollection(os); + // assert + expect(actual).to.equals(expected); + }); + }); + describe('ctor', () => { + describe('info', () => { + it('throws if undefined', () => { + // arrange + const expectedError = 'undefined project information'; + const info = undefined; + const collections = [new CategoryCollectionStub()]; + // act + const act = () => new Application(info, collections); + // assert + expect(act).to.throw(expectedError); + }); + it('sets as expected', () => { + // arrange + const expected = new ProjectInformationStub(); + const collections = [new CategoryCollectionStub()]; + // act + const sut = new Application(expected, collections); + // assert + expect(sut.info).to.equal(expected); + }); + }); + describe('collections', () => { + describe('throws on invalid value', () => { + // arrange + const testCases = [ + { + name: 'undefined', + expectedError: 'undefined collections', + value: undefined, + }, + { + name: 'empty', + expectedError: 'no collection in the list', + value: [], + }, + { + name: 'undefined value in list', + expectedError: 'undefined collection in the list', + value: [ new CategoryCollectionStub(), undefined ], + }, + { + name: 'two collections with same OS', + expectedError: 'multiple collections with same os: windows', + value: [ + new CategoryCollectionStub().withOs(OperatingSystem.Windows), + new CategoryCollectionStub().withOs(OperatingSystem.Windows), + new CategoryCollectionStub().withOs(OperatingSystem.BlackBerry), + ], + }, + ]; + for (const testCase of testCases) { + const info = new ProjectInformationStub(); + const collections = testCase.value; + // act + const act = () => new Application(info, collections); + // assert + expect(act).to.throw(testCase.expectedError); + } + }); + it('sets as expected', () => { + // arrange + const info = new ProjectInformationStub(); + const expected = [new CategoryCollectionStub()]; + // act + const sut = new Application(info, expected); + // assert + expect(sut.collections).to.equal(expected); + }); + }); + }); + describe('getSupportedOsList', () => { + it('returns expected', () => { + // arrange + const expected = [ OperatingSystem.Windows, OperatingSystem.macOS ]; + const info = new ProjectInformationStub(); + const collections = expected.map((os) => new CategoryCollectionStub().withOs(os)); + // act + const sut = new Application(info, collections); + const actual = sut.getSupportedOsList(); + // assert + expect(actual).to.deep.equal(expected); + }); + }); +}); diff --git a/tests/unit/domain/CategoryCollection.spec.ts b/tests/unit/domain/CategoryCollection.spec.ts index 265a1321..b71b8f06 100644 --- a/tests/unit/domain/CategoryCollection.spec.ts +++ b/tests/unit/domain/CategoryCollection.spec.ts @@ -1,16 +1,15 @@ -import { ScriptStub } from '../stubs/ScriptStub'; -import { CategoryStub } from '../stubs/CategoryStub'; import 'mocha'; import { expect } from 'chai'; import { ProjectInformation } from '@/domain/ProjectInformation'; -import { IProjectInformation } from '@/domain/IProjectInformation'; import { ICategory } from '@/domain/ICategory'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { RecommendationLevel } from '@/domain/RecommendationLevel'; import { getEnumValues } from '@/application/Common/Enum'; -import { CategoryCollection } from '../../../src/domain/CategoryCollection'; +import { CategoryCollection } from '@/domain/CategoryCollection'; +import { ScriptStub } from '../stubs/ScriptStub'; +import { CategoryStub } from '../stubs/CategoryStub'; describe('CategoryCollection', () => { describe('getScriptsByLevel', () => { @@ -177,31 +176,6 @@ describe('CategoryCollection', () => { expect(sut.totalCategories).to.equal(expected); }); }); - describe('information', () => { - it('sets information as expected', () => { - // arrange - const expected = new ProjectInformation( - 'expected-name', 'expected-repo', '0.31.0', 'expected-homepage'); - // act - const sut = new CategoryCollectionBuilder() - .withInfo(expected) - .construct(); - // assert - expect(sut.info).to.deep.equal(expected); - }); - it('cannot construct without information', () => { - // arrange - const information = undefined; - // act - function construct() { - return new CategoryCollectionBuilder() - .withInfo(information) - .construct(); - } - // assert - expect(construct).to.throw('undefined info'); - }); - }); describe('os', () => { it('sets os as expected', () => { // arrange @@ -281,7 +255,6 @@ function getValidScriptingDefinition(): IScriptingDefinition { class CategoryCollectionBuilder { private os = OperatingSystem.Windows; - private info = new ProjectInformation('name', 'repo', '0.1.0', 'homepage'); private actions: readonly ICategory[] = [ new CategoryStub(1).withScripts( new ScriptStub('S1').withLevel(RecommendationLevel.Standard), @@ -292,10 +265,6 @@ class CategoryCollectionBuilder { this.os = os; return this; } - public withInfo(info: IProjectInformation): CategoryCollectionBuilder { - this.info = info; - return this; - } public withActions(actions: readonly ICategory[]): CategoryCollectionBuilder { this.actions = actions; return this; @@ -305,6 +274,6 @@ class CategoryCollectionBuilder { return this; } public construct(): CategoryCollection { - return new CategoryCollection(this.os, this.info, this.actions, this.script); + return new CategoryCollection(this.os, this.actions, this.script); } } diff --git a/tests/unit/domain/ScriptCode.spec.ts b/tests/unit/domain/ScriptCode.spec.ts index b97f906d..47a8cbf1 100644 --- a/tests/unit/domain/ScriptCode.spec.ts +++ b/tests/unit/domain/ScriptCode.spec.ts @@ -1,7 +1,7 @@ import 'mocha'; import { expect } from 'chai'; import { ScriptCode } from '@/domain/ScriptCode'; -import { IScriptCode } from '../../../src/domain/IScriptCode'; +import { IScriptCode } from '@/domain/IScriptCode'; describe('ScriptCode', () => { describe('scriptName', () => { diff --git a/tests/unit/stubs/ApplicationStub.ts b/tests/unit/stubs/ApplicationStub.ts new file mode 100644 index 00000000..e97513a2 --- /dev/null +++ b/tests/unit/stubs/ApplicationStub.ts @@ -0,0 +1,28 @@ +import { IApplication } from '@/domain/IApplication'; +import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { IProjectInformation } from '@/domain/IProjectInformation'; +import { ProjectInformationStub } from './ProjectInformationStub'; + +export class ApplicationStub implements IApplication { + public info: IProjectInformation = new ProjectInformationStub(); + public collections: ICategoryCollection[] = [ ]; + public getCollection(operatingSystem: OperatingSystem): ICategoryCollection { + return this.collections.find((collection) => collection.os === operatingSystem); + } + public getSupportedOsList(): OperatingSystem[] { + return this.collections.map((collection) => collection.os); + } + public withCollection(collection: ICategoryCollection): ApplicationStub { + this.collections.push(collection); + return this; + } + public withProjectInformation(info: IProjectInformation): ApplicationStub { + this.info = info; + return this; + } + public withCollections(...collections: readonly ICategoryCollection[]): ApplicationStub { + this.collections.push(...collections); + return this; + } +} diff --git a/tests/unit/stubs/CategoryCollectionStub.ts b/tests/unit/stubs/CategoryCollectionStub.ts index 67a299a6..920ed3db 100644 --- a/tests/unit/stubs/CategoryCollectionStub.ts +++ b/tests/unit/stubs/CategoryCollectionStub.ts @@ -1,5 +1,4 @@ import { ScriptingDefinitionStub } from './ScriptingDefinitionStub'; -import { ProjectInformation } from '@/domain/ProjectInformation'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { ScriptStub } from './ScriptStub'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; @@ -13,7 +12,6 @@ export class CategoryCollectionStub implements ICategoryCollection { public initialScript: IScript = new ScriptStub('55'); public totalScripts = 0; public totalCategories = 0; - public readonly info = new ProjectInformation('StubApplication', '0.1.0', 'https://github.com/undergroundwires/privacy.sexy', 'https://privacy.sexy'); public readonly actions = new Array(); public withAction(category: ICategory): CategoryCollectionStub { diff --git a/tests/unit/stubs/EnvironmentStub.ts b/tests/unit/stubs/EnvironmentStub.ts new file mode 100644 index 00000000..c21940b1 --- /dev/null +++ b/tests/unit/stubs/EnvironmentStub.ts @@ -0,0 +1,11 @@ +import { IEnvironment } from '@/application/Environment/IEnvironment'; +import { OperatingSystem } from '@/domain/OperatingSystem'; + +export class EnvironmentStub implements IEnvironment { + public isDesktop = true; + public os = OperatingSystem.Windows; + public withOs(os: OperatingSystem): EnvironmentStub { + this.os = os; + return this; + } +} diff --git a/tests/unit/stubs/ProcessEnvironmentStub.ts b/tests/unit/stubs/ProcessEnvironmentStub.ts new file mode 100644 index 00000000..25146f58 --- /dev/null +++ b/tests/unit/stubs/ProcessEnvironmentStub.ts @@ -0,0 +1,8 @@ +export function getProcessEnvironmentStub(): NodeJS.ProcessEnv { + return { + VUE_APP_VERSION: 'stub-version', + VUE_APP_NAME: 'stub-name', + VUE_APP_REPOSITORY_URL: 'stub-repository-url', + VUE_APP_HOMEPAGE_URL: 'stub-homepage-url', + }; +} diff --git a/tests/unit/stubs/ProjectInformationStub.ts b/tests/unit/stubs/ProjectInformationStub.ts index ec0565f9..1c55a9a5 100644 --- a/tests/unit/stubs/ProjectInformationStub.ts +++ b/tests/unit/stubs/ProjectInformationStub.ts @@ -2,13 +2,13 @@ import { IProjectInformation } from '@/domain/IProjectInformation'; import { OperatingSystem } from '@/domain/OperatingSystem'; export class ProjectInformationStub implements IProjectInformation { - public name: string; - public version: string; - public repositoryUrl: string; - public homepage: string; - public feedbackUrl: string; - public releaseUrl: string; - public repositoryWebUrl: string; + public name = 'name'; + public version = 'version'; + public repositoryUrl = 'repositoryUrl'; + public homepage = 'homepage'; + public feedbackUrl = 'feedbackUrl'; + public releaseUrl = 'releaseUrl'; + public repositoryWebUrl = 'repositoryWebUrl'; public withName(name: string): ProjectInformationStub { this.name = name; return this; diff --git a/tests/unit/stubs/YamlApplicationStub.ts b/tests/unit/stubs/YamlApplicationStub.ts new file mode 100644 index 00000000..84c7ba5d --- /dev/null +++ b/tests/unit/stubs/YamlApplicationStub.ts @@ -0,0 +1,53 @@ +import { RecommendationLevel } from '@/domain/RecommendationLevel'; +import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; +import { YamlCategory, YamlScript, YamlApplication, YamlScriptingDefinition } from 'js-yaml-loader!@/application/application.yaml'; + +export class YamlApplicationStub implements YamlApplication { + public os = 'windows'; + public actions: readonly YamlCategory[] = [ getCategoryStub() ]; + public scripting: YamlScriptingDefinition = getTestDefinitionStub(); + + public withActions(actions: readonly YamlCategory[]): YamlApplicationStub { + this.actions = actions; + return this; + } + + public withOs(os: string): YamlApplicationStub { + this.os = os; + return this; + } + + public withScripting(scripting: YamlScriptingDefinition): YamlApplicationStub { + this.scripting = scripting; + return this; + } +} + +export function getCategoryStub(scriptPrefix = 'testScript'): YamlCategory { + return { + category: 'category name', + children: [ + getScriptStub(`${scriptPrefix}-standard`, RecommendationLevel.Standard), + getScriptStub(`${scriptPrefix}-strict`, RecommendationLevel.Strict), + ], + }; +} + +function getTestDefinitionStub(): YamlScriptingDefinition { + return { + fileExtension: '.bat', + language: ScriptingLanguage[ScriptingLanguage.batchfile], + startCode: 'start', + endCode: 'end', + }; +} + +function getScriptStub(scriptName: string, level: RecommendationLevel = RecommendationLevel.Standard): YamlScript { + return { + name: scriptName, + code: 'script code', + revertCode: 'revert code', + recommend: RecommendationLevel[level].toLowerCase(), + call: undefined, + }; +}