refactor to allow switching ICategoryCollection context #40
This commit is contained in:
@@ -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)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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<OperatingSystem, ICategoryCollectionState>;
|
||||
|
||||
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<IApplicationContextChangedEvent>();
|
||||
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<OperatingSystem, ICategoryCollectionState>();
|
||||
for (const collection of app.collections) {
|
||||
machine[collection.os] = new CategoryCollectionState(collection);
|
||||
}
|
||||
return machine;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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<IApplicationContextChangedEvent>;
|
||||
changeContext(os: OperatingSystem): void;
|
||||
}
|
||||
|
||||
export interface IApplicationContextChangedEvent {
|
||||
readonly newState: ICategoryCollectionState;
|
||||
readonly newCollection: ICategoryCollection;
|
||||
readonly newOs: OperatingSystem;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IEnvironment {
|
||||
isDesktop: boolean;
|
||||
os: OperatingSystem;
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem;
|
||||
}
|
||||
|
||||
27
src/application/Parser/ApplicationParser.ts
Normal file
27
src/application/Parser/ApplicationParser.ts
Normal file
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
47
src/domain/Application.ts
Normal file
47
src/domain/Application.ts
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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<ICategory>,
|
||||
public readonly scripting: IScriptingDefinition) {
|
||||
if (!info) {
|
||||
throw new Error('undefined info');
|
||||
}
|
||||
if (!scripting) {
|
||||
throw new Error('undefined scripting definition');
|
||||
}
|
||||
|
||||
11
src/domain/IApplication.ts
Normal file
11
src/domain/IApplication.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -39,7 +39,7 @@ export default class DownloadUrlListItem extends StatefulVue {
|
||||
|
||||
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
return context.collection.info.getDownloadUrl(os);
|
||||
return context.app.info.getDownloadUrl(os);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
272
tests/unit/application/Context/ApplicationContext.spec.ts
Normal file
272
tests/unit/application/Context/ApplicationContext.spec.ts
Normal file
@@ -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<IApplicationContextChangedEvent>();
|
||||
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<T>(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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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', () => {
|
||||
|
||||
108
tests/unit/application/Parser/ApplicationParser.spec.ts
Normal file
108
tests/unit/application/Parser/ApplicationParser.spec.ts
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<TId>(array: ReadonlyArray<IEntity<TId>>) {
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
118
tests/unit/domain/Application.spec.ts
Normal file
118
tests/unit/domain/Application.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
28
tests/unit/stubs/ApplicationStub.ts
Normal file
28
tests/unit/stubs/ApplicationStub.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ICategory>();
|
||||
|
||||
public withAction(category: ICategory): CategoryCollectionStub {
|
||||
|
||||
11
tests/unit/stubs/EnvironmentStub.ts
Normal file
11
tests/unit/stubs/EnvironmentStub.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
8
tests/unit/stubs/ProcessEnvironmentStub.ts
Normal file
8
tests/unit/stubs/ProcessEnvironmentStub.ts
Normal file
@@ -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',
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
53
tests/unit/stubs/YamlApplicationStub.ts
Normal file
53
tests/unit/stubs/YamlApplicationStub.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user