diff --git a/README.md b/README.md
index b433bf8b..994d3b3a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# privacy.sexy
-> Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
+> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
[](./CONTRIBUTING.md)
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
diff --git a/package-lock.json b/package-lock.json
index a160a030..3e51be5f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "privacy.sexy",
- "version": "0.8.1",
+ "version": "0.8.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index edf8d66f..bef4564a 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "privacy.sexy",
"version": "0.8.2",
"author": "undergroundwires",
- "description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
+ "description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
"homepage": "https://privacy.sexy",
"private": true,
"repository": {
diff --git a/public/index.html b/public/index.html
index 530f2b0d..3223b92e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,7 +4,7 @@
-
Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows
+ Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows and macOS
diff --git a/src/App.vue b/src/App.vue
index f66297e9..83ce3ec6 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -16,7 +16,7 @@ import { Component, Vue } from 'vue-property-decorator';
import TheHeader from '@/presentation/TheHeader.vue';
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
import TheCodeArea from '@/presentation/TheCodeArea.vue';
-import TheCodeButtons from '@/presentation/TheCodeButtons.vue';
+import TheCodeButtons from '@/presentation/CodeButtons/TheCodeButtons.vue';
import TheSearchBar from '@/presentation/TheSearchBar.vue';
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
diff --git a/src/application/Context/ApplicationContext.ts b/src/application/Context/ApplicationContext.ts
index 9b15acfa..de3e9b68 100644
--- a/src/application/Context/ApplicationContext.ts
+++ b/src/application/Context/ApplicationContext.ts
@@ -36,9 +36,8 @@ export class ApplicationContext implements IApplicationContext {
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
}
const event: IApplicationContextChangedEvent = {
- newState: this.state,
- newCollection: this.collection,
- newOs: os,
+ newState: this.states[os],
+ oldState: this.states[this.currentOs],
};
this.contextChanged.notify(event);
this.currentOs = os;
diff --git a/src/application/Context/ApplicationContextProvider.ts b/src/application/Context/ApplicationContextProvider.ts
index 05f91eec..1a8bb4ff 100644
--- a/src/application/Context/ApplicationContextProvider.ts
+++ b/src/application/Context/ApplicationContextProvider.ts
@@ -24,9 +24,8 @@ function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSy
return currentOs;
}
supportedOsList.sort((os1, os2) => {
- const os1SupportLevel = app.collections[os1].totalScripts;
- const os2SupportLevel = app.collections[os2].totalScripts;
- return os1SupportLevel - os2SupportLevel;
+ const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
+ return getPriority(os2) - getPriority(os1);
});
return supportedOsList[0];
}
diff --git a/src/application/Context/IApplicationContext.ts b/src/application/Context/IApplicationContext.ts
index 24a6d66b..ff5a943e 100644
--- a/src/application/Context/IApplicationContext.ts
+++ b/src/application/Context/IApplicationContext.ts
@@ -1,13 +1,10 @@
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;
@@ -15,6 +12,5 @@ export interface IApplicationContext {
export interface IApplicationContextChangedEvent {
readonly newState: ICategoryCollectionState;
- readonly newCollection: ICategoryCollection;
- readonly newOs: OperatingSystem;
+ readonly oldState: ICategoryCollectionState;
}
diff --git a/src/application/Context/State/CategoryCollectionState.ts b/src/application/Context/State/CategoryCollectionState.ts
index ee30e8af..0f31539e 100644
--- a/src/application/Context/State/CategoryCollectionState.ts
+++ b/src/application/Context/State/CategoryCollectionState.ts
@@ -6,8 +6,10 @@ import { IUserSelection } from './Selection/IUserSelection';
import { ICategoryCollectionState } from './ICategoryCollectionState';
import { IApplicationCode } from './Code/IApplicationCode';
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
+import { OperatingSystem } from '@/domain/OperatingSystem';
export class CategoryCollectionState implements ICategoryCollectionState {
+ public readonly os: OperatingSystem;
public readonly code: IApplicationCode;
public readonly selection: IUserSelection;
public readonly filter: IUserFilter;
@@ -16,5 +18,6 @@ export class CategoryCollectionState implements ICategoryCollectionState {
this.selection = new UserSelection(collection, []);
this.code = new ApplicationCode(this.selection, collection.scripting);
this.filter = new UserFilter(collection);
+ this.os = collection.os;
}
}
diff --git a/src/application/Context/State/Code/Generation/CodeBuilder.ts b/src/application/Context/State/Code/Generation/CodeBuilder.ts
index 5354035e..601352e1 100644
--- a/src/application/Context/State/Code/Generation/CodeBuilder.ts
+++ b/src/application/Context/State/Code/Generation/CodeBuilder.ts
@@ -3,7 +3,7 @@ import { ICodeBuilder } from './ICodeBuilder';
const NewLine = '\n';
const TotalFunctionSeparatorChars = 58;
-export class CodeBuilder implements ICodeBuilder {
+export abstract class CodeBuilder implements ICodeBuilder {
private readonly lines = new Array();
// Returns current line starting from 0 (no lines), or 1 (have single line)
@@ -29,7 +29,7 @@ export class CodeBuilder implements ICodeBuilder {
}
public appendCommentLine(commentLine?: string): CodeBuilder {
- this.lines.push(`:: ${commentLine}`);
+ this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
return this;
}
@@ -37,9 +37,8 @@ export class CodeBuilder implements ICodeBuilder {
if (!name) { throw new Error('name cannot be empty or null'); }
if (!code) { throw new Error('code cannot be empty or null'); }
return this
- .appendLine()
.appendCommentLineWithHyphensAround(name)
- .appendLine(`echo --- ${name}`)
+ .appendLine(this.writeStandardOut(`--- ${name}`))
.appendLine(code)
.appendTrailingHyphensCommentLine();
}
@@ -62,4 +61,7 @@ export class CodeBuilder implements ICodeBuilder {
public toString(): string {
return this.lines.join(NewLine);
}
+
+ protected abstract getCommentDelimiter(): string;
+ protected abstract writeStandardOut(text: string): string;
}
diff --git a/src/application/Context/State/Code/Generation/CodeBuilderFactory.ts b/src/application/Context/State/Code/Generation/CodeBuilderFactory.ts
new file mode 100644
index 00000000..040d21d2
--- /dev/null
+++ b/src/application/Context/State/Code/Generation/CodeBuilderFactory.ts
@@ -0,0 +1,15 @@
+import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
+import { ICodeBuilder } from './ICodeBuilder';
+import { ICodeBuilderFactory } from './ICodeBuilderFactory';
+import { BatchBuilder } from './Languages/BatchBuilder';
+import { ShellBuilder } from './Languages/ShellBuilder';
+
+export class CodeBuilderFactory implements ICodeBuilderFactory {
+ public create(language: ScriptingLanguage): ICodeBuilder {
+ switch (language) {
+ case ScriptingLanguage.shellscript: return new ShellBuilder();
+ case ScriptingLanguage.batchfile: return new BatchBuilder();
+ default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
+ }
+ }
+}
diff --git a/src/application/Context/State/Code/Generation/ICodeBuilderFactory.ts b/src/application/Context/State/Code/Generation/ICodeBuilderFactory.ts
new file mode 100644
index 00000000..116ddb9c
--- /dev/null
+++ b/src/application/Context/State/Code/Generation/ICodeBuilderFactory.ts
@@ -0,0 +1,6 @@
+import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
+import { ICodeBuilder } from './ICodeBuilder';
+
+export interface ICodeBuilderFactory {
+ create(language: ScriptingLanguage): ICodeBuilder;
+}
diff --git a/src/application/Context/State/Code/Generation/Languages/BatchBuilder.ts b/src/application/Context/State/Code/Generation/Languages/BatchBuilder.ts
new file mode 100644
index 00000000..7a3668ef
--- /dev/null
+++ b/src/application/Context/State/Code/Generation/Languages/BatchBuilder.ts
@@ -0,0 +1,10 @@
+import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
+
+export class BatchBuilder extends CodeBuilder {
+ protected getCommentDelimiter(): string {
+ return '::';
+ }
+ protected writeStandardOut(text: string): string {
+ return `echo ${text}`;
+ }
+}
diff --git a/src/application/Context/State/Code/Generation/Languages/ShellBuilder.ts b/src/application/Context/State/Code/Generation/Languages/ShellBuilder.ts
new file mode 100644
index 00000000..b8209569
--- /dev/null
+++ b/src/application/Context/State/Code/Generation/Languages/ShellBuilder.ts
@@ -0,0 +1,10 @@
+import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
+
+export class ShellBuilder extends CodeBuilder {
+ protected getCommentDelimiter(): string {
+ return '#';
+ }
+ protected writeStandardOut(text: string): string {
+ return `echo '${text}'`;
+ }
+}
diff --git a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts
index 4fe13583..e3edd277 100644
--- a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts
+++ b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts
@@ -1,14 +1,15 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IUserScriptGenerator } from './IUserScriptGenerator';
-import { CodeBuilder } from './CodeBuilder';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScript } from './IUserScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ICodeBuilder } from './ICodeBuilder';
+import { ICodeBuilderFactory } from './ICodeBuilderFactory';
+import { CodeBuilderFactory } from './CodeBuilderFactory';
export class UserScriptGenerator implements IUserScriptGenerator {
- constructor(private readonly codeBuilderFactory: () => ICodeBuilder = () => new CodeBuilder()) {
+ constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
}
public buildCode(
@@ -20,7 +21,7 @@ export class UserScriptGenerator implements IUserScriptGenerator {
if (!selectedScripts.length) {
return { code: '', scriptPositions };
}
- let builder = this.codeBuilderFactory();
+ let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
builder = initializeCode(scriptingDefinition.startCode, builder);
for (const selection of selectedScripts) {
scriptPositions = appendSelection(selection, scriptPositions, builder);
@@ -52,16 +53,19 @@ function appendSelection(
selection: SelectedScript,
scriptPositions: Map,
builder: ICodeBuilder): Map {
- const startPosition = builder.currentLine + 1;
- appendCode(selection, builder);
+ const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
+ builder = appendCode(selection, builder);
const endPosition = builder.currentLine - 1;
builder.appendLine();
- scriptPositions.set(selection, new CodePosition(startPosition, endPosition));
+ const position = new CodePosition(startPosition, endPosition);
+ scriptPositions.set(selection, position);
return scriptPositions;
}
-function appendCode(selection: SelectedScript, builder: ICodeBuilder) {
+function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
- builder.appendFunction(name, scriptCode);
+ return builder
+ .appendLine()
+ .appendFunction(name, scriptCode);
}
diff --git a/src/application/Context/State/Code/Position/CodePosition.ts b/src/application/Context/State/Code/Position/CodePosition.ts
index ad2a9451..c0921989 100644
--- a/src/application/Context/State/Code/Position/CodePosition.ts
+++ b/src/application/Context/State/Code/Position/CodePosition.ts
@@ -1,6 +1,6 @@
import { ICodePosition } from './ICodePosition';
-export class CodePosition implements ICodePosition {
+export class CodePosition implements ICodePosition {
public get totalLines(): number {
return this.endLine - this.startLine;
}
diff --git a/src/application/Context/State/Filter/IUserFilter.ts b/src/application/Context/State/Filter/IUserFilter.ts
index 212b333d..e87e6f6f 100644
--- a/src/application/Context/State/Filter/IUserFilter.ts
+++ b/src/application/Context/State/Filter/IUserFilter.ts
@@ -1,5 +1,5 @@
+import { ISignal } from '@/infrastructure/Events/ISignal';
import { IFilterResult } from './IFilterResult';
-import { ISignal } from '@/infrastructure/Events/Signal';
export interface IUserFilter {
readonly currentFilter: IFilterResult | undefined;
diff --git a/src/application/Context/State/ICategoryCollectionState.ts b/src/application/Context/State/ICategoryCollectionState.ts
index 21482f8f..af846bc6 100644
--- a/src/application/Context/State/ICategoryCollectionState.ts
+++ b/src/application/Context/State/ICategoryCollectionState.ts
@@ -1,10 +1,13 @@
import { IUserFilter } from './Filter/IUserFilter';
import { IUserSelection } from './Selection/IUserSelection';
import { IApplicationCode } from './Code/IApplicationCode';
-export { IUserSelection, IApplicationCode, IUserFilter };
+import { ICategoryCollection } from '@/domain/ICategoryCollection';
+import { OperatingSystem } from '@/domain/OperatingSystem';
export interface ICategoryCollectionState {
readonly code: IApplicationCode;
readonly filter: IUserFilter;
readonly selection: IUserSelection;
+ readonly collection: ICategoryCollection;
+ readonly os: OperatingSystem;
}
diff --git a/src/application/Context/State/Selection/IUserSelection.ts b/src/application/Context/State/Selection/IUserSelection.ts
index a817f6e9..c8f95599 100644
--- a/src/application/Context/State/Selection/IUserSelection.ts
+++ b/src/application/Context/State/Selection/IUserSelection.ts
@@ -1,7 +1,7 @@
import { SelectedScript } from './SelectedScript';
-import { ISignal } from '@/infrastructure/Events/Signal';
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
+import { ISignal } from '@/infrastructure/Events/ISignal';
export interface IUserSelection {
readonly changed: ISignal>;
diff --git a/src/application/Parser/ApplicationParser.ts b/src/application/Parser/ApplicationParser.ts
index 2803304f..28f6464e 100644
--- a/src/application/Parser/ApplicationParser.ts
+++ b/src/application/Parser/ApplicationParser.ts
@@ -3,6 +3,7 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { parseCategoryCollection } from './CategoryCollectionParser';
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
+import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
import { CollectionData } from 'js-yaml-loader!@/*';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { Application } from '@/domain/Application';
@@ -10,10 +11,11 @@ import { Application } from '@/domain/Application';
export function parseApplication(
parser = CategoryCollectionParser,
processEnv: NodeJS.ProcessEnv = process.env,
- collectionData = LoadedCollectionData): IApplication {
+ collectionsData = PreParsedCollections): IApplication {
+ validateCollectionsData(collectionsData);
const information = parseProjectInformation(processEnv);
- const collection = parser(collectionData, information);
- const app = new Application(information, [ collection ]);
+ const collections = collectionsData.map((collection) => parser(collection, information));
+ const app = new Application(information, collections);
return app;
}
@@ -23,5 +25,14 @@ export type CategoryCollectionParserType
const CategoryCollectionParser: CategoryCollectionParserType
= (file, info) => parseCategoryCollection(file, info);
-const LoadedCollectionData: CollectionData
- = WindowsData;
+const PreParsedCollections: readonly CollectionData []
+ = [ WindowsData, MacOsData ];
+
+function validateCollectionsData(collections: readonly CollectionData[]) {
+ if (!collections.length) {
+ throw new Error('no collection provided');
+ }
+ if (collections.some((collection) => !collection)) {
+ throw new Error('undefined collection provided');
+ }
+}
diff --git a/src/application/Parser/CategoryCollectionParser.ts b/src/application/Parser/CategoryCollectionParser.ts
index 74a4697a..373e871c 100644
--- a/src/application/Parser/CategoryCollectionParser.ts
+++ b/src/application/Parser/CategoryCollectionParser.ts
@@ -1,27 +1,27 @@
import { Category } from '@/domain/Category';
import { CollectionData } from 'js-yaml-loader!@/*';
import { parseCategory } from './CategoryParser';
-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';
+import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
export function parseCategoryCollection(
content: CollectionData,
info: IProjectInformation,
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
validate(content);
- const compiler = new ScriptCompiler(content.functions);
+ const scripting = parseScriptingDefinition(content.scripting, info);
+ const context = new CategoryCollectionParseContext(content.functions, scripting);
const categories = new Array();
for (const action of content.actions) {
- const category = parseCategory(action, compiler);
+ const category = parseCategory(action, context);
categories.push(category);
}
const os = osParser.parseEnum(content.os, 'os');
- const scripting = parseScriptingDefinition(content.scripting, info);
const collection = new CategoryCollection(
os,
categories,
diff --git a/src/application/Parser/CategoryParser.ts b/src/application/Parser/CategoryParser.ts
index ee0d8ac3..2ccaadb4 100644
--- a/src/application/Parser/CategoryParser.ts
+++ b/src/application/Parser/CategoryParser.ts
@@ -2,8 +2,8 @@ import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@
import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category';
import { parseDocUrls } from './DocumentationParser';
-import { parseScript } from './ScriptParser';
-import { IScriptCompiler } from './Compiler/IScriptCompiler';
+import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
+import { parseScript } from './Script/ScriptParser';
let categoryIdCounter: number = 0;
@@ -12,17 +12,15 @@ interface ICategoryChildren {
subScripts: Script[];
}
-export function parseCategory(category: CategoryData, compiler: IScriptCompiler): Category {
- if (!compiler) {
- throw new Error('undefined compiler');
- }
+export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
+ if (!context) { throw new Error('undefined context'); }
ensureValid(category);
const children: ICategoryChildren = {
subCategories: new Array(),
subScripts: new Array
+
+
diff --git a/src/presentation/IconButton.vue b/src/presentation/CodeButtons/IconButton.vue
similarity index 89%
rename from src/presentation/IconButton.vue
rename to src/presentation/CodeButtons/IconButton.vue
index f6c60190..631aae99 100644
--- a/src/presentation/IconButton.vue
+++ b/src/presentation/CodeButtons/IconButton.vue
@@ -8,11 +8,10 @@
diff --git a/src/presentation/CodeButtons/MacOsInstructions.vue b/src/presentation/CodeButtons/MacOsInstructions.vue
new file mode 100644
index 00000000..07f6fae9
--- /dev/null
+++ b/src/presentation/CodeButtons/MacOsInstructions.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+ -
+ Download the file
+
+
+ -
+ Open terminal
+
+
+ -
+ Navigate to the folder where you downloaded the file e.g.:
+
+ cd ~/Downloads
+
+
+
+ -
+ Give the file execute permissions:
+
+ chmod +x {{ this.fileName }}
+
+
+
+ -
+ Execute the file:
+
+ ./{{ this.fileName }}
+
+
+
+ -
+ If asked, enter your administrator password
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/presentation/CodeButtons/TheCodeButtons.vue b/src/presentation/CodeButtons/TheCodeButtons.vue
new file mode 100644
index 00000000..e22ed309
--- /dev/null
+++ b/src/presentation/CodeButtons/TheCodeButtons.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
diff --git a/src/presentation/Scripts/Cards/CardList.vue b/src/presentation/Scripts/Cards/CardList.vue
index d61e7f97..f39c45e4 100644
--- a/src/presentation/Scripts/Cards/CardList.vue
+++ b/src/presentation/Scripts/Cards/CardList.vue
@@ -21,6 +21,8 @@ import CardListItem from './CardListItem.vue';
import { StatefulVue } from '@/presentation/StatefulVue';
import { ICategory } from '@/domain/ICategory';
import { hasDirective } from './NonCollapsingDirective';
+import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
+import { IApplication } from '@/domain/IApplication';
@Component({
components: {
@@ -31,9 +33,7 @@ export default class CardList extends StatefulVue {
public categoryIds: number[] = [];
public activeCategoryId?: number = null;
- public async mounted() {
- const context = await this.getCurrentContextAsync();
- this.setCategories(context.collection.actions);
+ public created() {
this.onOutsideOfActiveCardClicked((element) => {
if (hasDirective(element)) {
return;
@@ -41,15 +41,21 @@ export default class CardList extends StatefulVue {
this.activeCategoryId = null;
});
}
-
public onSelected(categoryId: number, isExpanded: boolean) {
this.activeCategoryId = isExpanded ? categoryId : undefined;
}
+ protected initialize(app: IApplication): void {
+ return;
+ }
+ protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
+ this.setCategories(newState.collection.actions);
+ this.activeCategoryId = undefined;
+ }
+
private setCategories(categories: ReadonlyArray): void {
this.categoryIds = categories.map((category) => category.id);
}
-
private onOutsideOfActiveCardClicked(callback: (clickedElement: Element) => void) {
const outsideClickListener = (event) => {
if (!this.activeCategoryId) {
diff --git a/src/presentation/Scripts/Cards/CardListItem.vue b/src/presentation/Scripts/Cards/CardListItem.vue
index 092752c6..31639a8a 100644
--- a/src/presentation/Scripts/Cards/CardListItem.vue
+++ b/src/presentation/Scripts/Cards/CardListItem.vue
@@ -49,16 +49,17 @@ export default class CardListItem extends StatefulVue {
public isAnyChildSelected = false;
public areAllChildrenSelected = false;
-@Emit('selected')
+ public async mounted() {
+ this.updateStateAsync(this.categoryId);
+ }
+ @Emit('selected')
public onSelected(isExpanded: boolean) {
this.isExpanded = isExpanded;
}
-
@Watch('activeCategoryId')
public async onActiveCategoryChanged(value: |number) {
this.isExpanded = value === this.categoryId;
}
-
@Watch('isExpanded')
public async onExpansionChangedAsync(newValue: number, oldValue: number) {
if (!oldValue && newValue) {
@@ -67,24 +68,22 @@ export default class CardListItem extends StatefulVue {
(focusElement as HTMLElement).scrollIntoView({behavior: 'smooth'});
}
}
-
- public async mounted() {
- const context = await this.getCurrentContextAsync();
- context.state.selection.changed.on(() => {
- this.updateStateAsync(this.categoryId);
- });
- this.updateStateAsync(this.categoryId);
- }
-
@Watch('categoryId')
public async updateStateAsync(value: |number) {
const context = await this.getCurrentContextAsync();
- const category = !value ? undefined : context.collection.findCategory(this.categoryId);
+ const category = !value ? undefined : context.state.collection.findCategory(this.categoryId);
this.cardTitle = category ? category.name : undefined;
const currentSelection = context.state.selection;
this.isAnyChildSelected = category ? currentSelection.isAnySelected(category) : false;
this.areAllChildrenSelected = category ? currentSelection.areAllSelected(category) : false;
}
+ protected initialize(): void {
+ return;
+ }
+ protected handleCollectionState(): void {
+ // No need, as categoryId will be updated instead
+ return;
+ }
}
diff --git a/src/presentation/Scripts/Grouping/TheGrouper.vue b/src/presentation/Scripts/Grouping/TheGrouper.vue
index 553680e5..e77e24dd 100644
--- a/src/presentation/Scripts/Grouping/TheGrouper.vue
+++ b/src/presentation/Scripts/Grouping/TheGrouper.vue
@@ -15,15 +15,13 @@
diff --git a/src/presentation/Scripts/TheScripts.vue b/src/presentation/Scripts/TheScripts.vue
index 8795fc37..6c84c31b 100644
--- a/src/presentation/Scripts/TheScripts.vue
+++ b/src/presentation/Scripts/TheScripts.vue
@@ -1,10 +1,12 @@
-
-
+
+
+
+ v-if="!this.isSearching" />
@@ -37,72 +39,93 @@
+
\ No newline at end of file
diff --git a/src/presentation/StatefulVue.ts b/src/presentation/StatefulVue.ts
index e7b0faac..36061b91 100644
--- a/src/presentation/StatefulVue.ts
+++ b/src/presentation/StatefulVue.ts
@@ -1,13 +1,40 @@
-import { Vue } from 'vue-property-decorator';
+import { Component, Vue } from 'vue-property-decorator';
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { buildContext } from '@/application/Context/ApplicationContextProvider';
+import { IApplicationContextChangedEvent } from '../application/Context/IApplicationContext';
+import { IApplication } from '@/domain/IApplication';
+import { ICategoryCollectionState } from '../application/Context/State/ICategoryCollectionState';
+import { IEventSubscription } from '../infrastructure/Events/ISubscription';
+// @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91
+@Component
export abstract class StatefulVue extends Vue {
- private static instance = new AsyncLazy
(
+ public static instance = new AsyncLazy(
() => Promise.resolve(buildContext()));
+ private listener: IEventSubscription;
+
+ public async mounted() {
+ const context = await this.getCurrentContextAsync();
+ this.listener = context.contextChanged.on((event) => this.handleStateChangedEvent(event));
+ this.initialize(context.app);
+ this.handleCollectionState(context.state, undefined);
+ }
+ public destroyed() {
+ if (this.listener) {
+ this.listener.unsubscribe();
+ }
+ }
+
+ protected abstract initialize(app: IApplication): void;
+ protected abstract handleCollectionState(
+ newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined): void;
protected getCurrentContextAsync(): Promise {
return StatefulVue.instance.getValueAsync();
}
+
+ private handleStateChangedEvent(event: IApplicationContextChangedEvent) {
+ this.handleCollectionState(event.newState, event.oldState);
+ }
}
diff --git a/src/presentation/TheCodeArea.vue b/src/presentation/TheCodeArea.vue
index 26bd11f9..b4c54b35 100644
--- a/src/presentation/TheCodeArea.vue
+++ b/src/presentation/TheCodeArea.vue
@@ -7,27 +7,14 @@ import { Component, Prop } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue';
import ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
-import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
import { IScript } from '@/domain/IScript';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
-
-const NothingChosenCode =
- new CodeBuilder()
- .appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows')
- .appendLine()
- .appendCommentLine('-- 🤔 How to use')
- .appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
- .appendCommentLine(' 📙 On top left, you can apply predefined selections for privacy level you\'d like.')
- .appendCommentLine(' 📙 After you choose any tweak, you can download or copy to execute your script.')
- .appendCommentLine(' 📙 Come back regularly to apply latest version for stronger privacy and security.')
- .appendLine()
- .appendCommentLine('-- 🧐 Why privacy.sexy')
- .appendCommentLine(' ✔️ Rich tweak pool to harden security & privacy of the OS and other software on it.')
- .appendCommentLine(' ✔️ No need to run any compiled software on your system, just run the generated scripts.')
- .appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
- .appendCommentLine(' ✔️ Open-source and free (both free as in beer and free as in speech).')
- .toString();
+import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
+import { IApplication } from '@/domain/IApplication';
+import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
+import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
+import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
@Component
export default class TheCodeArea extends StatefulVue {
@@ -35,21 +22,41 @@ export default class TheCodeArea extends StatefulVue {
private editor!: ace.Ace.Editor;
private currentMarkerId?: number;
+ private codeListener: IEventSubscription;
@Prop() private theme!: string;
- public async mounted() {
- const context = await this.getCurrentContextAsync();
- this.editor = initializeEditor(this.theme, this.editorId, context.collection.scripting.language);
- const appCode = context.state.code;
- this.editor.setValue(appCode.current || NothingChosenCode, 1);
- appCode.changed.on((code) => this.updateCode(code));
+ public destroyed() {
+ this.unsubscribeCodeListening();
+ this.destroyEditor();
}
- private updateCode(event: ICodeChangedEvent) {
+ protected initialize(app: IApplication): void {
+ return;
+ }
+ protected handleCollectionState(newState: ICategoryCollectionState): void {
+ this.destroyEditor();
+ this.editor = initializeEditor(this.theme, this.editorId, newState.collection.scripting.language);
+ const appCode = newState.code;
+ this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1);
+ this.unsubscribeCodeListening();
+ this.subscribe(appCode);
+ }
+
+ private subscribe(appCode: IApplicationCode) {
+ this.codeListener = appCode.changed.on((code) => this.updateCodeAsync(code));
+ }
+ private unsubscribeCodeListening() {
+ if (this.codeListener) {
+ this.codeListener.unsubscribe();
+ }
+ }
+ private async updateCodeAsync(event: ICodeChangedEvent) {
this.removeCurrentHighlighting();
if (event.isEmpty()) {
- this.editor.setValue(NothingChosenCode, 1);
+ const context = await this.getCurrentContextAsync();
+ const defaultCode = getDefaultCode(context.state.collection.scripting.language);
+ this.editor.setValue(defaultCode, 1);
return;
}
this.editor.setValue(event.code, 1);
@@ -60,7 +67,6 @@ export default class TheCodeArea extends StatefulVue {
this.reactToChanges(event, event.changedScripts);
}
}
-
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray) {
const positions = scripts
.map((script) => event.getScriptPositionInCode(script));
@@ -73,19 +79,16 @@ export default class TheCodeArea extends StatefulVue {
this.scrollToLine(end + 2);
this.highlight(start, end);
}
-
private highlight(startRow: number, endRow: number) {
const AceRange = ace.require('ace/range').Range;
this.currentMarkerId = this.editor.session.addMarker(
new AceRange(startRow, 0, endRow, 0), 'code-area__highlight', 'fullLine',
);
}
-
private scrollToLine(row: number) {
const column = this.editor.session.getLine(row).length;
this.editor.gotoLine(row, column, true);
}
-
private removeCurrentHighlighting() {
if (!this.currentMarkerId) {
return;
@@ -93,6 +96,11 @@ export default class TheCodeArea extends StatefulVue {
this.editor.session.removeMarker(this.currentMarkerId);
this.currentMarkerId = undefined;
}
+ private destroyEditor() {
+ if (this.editor) {
+ this.editor.destroy();
+ }
+ }
}
function initializeEditor(theme: string, editorId: string, language: ScriptingLanguage): ace.Ace.Editor {
@@ -109,13 +117,32 @@ function initializeEditor(theme: string, editorId: string, language: ScriptingLa
function getLanguage(language: ScriptingLanguage) {
switch (language) {
- case ScriptingLanguage.batchfile:
- return 'batchfile';
+ case ScriptingLanguage.batchfile: return 'batchfile';
+ case ScriptingLanguage.shellscript: return 'sh';
default:
- throw new Error('unkown language');
+ throw new Error('unknown language');
}
}
+function getDefaultCode(language: ScriptingLanguage): string {
+ return new CodeBuilderFactory()
+ .create(language)
+ .appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows and macOS')
+ .appendLine()
+ .appendCommentLine('-- 🤔 How to use')
+ .appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
+ .appendCommentLine(' 📙 On top left, you can apply predefined selections for privacy level you\'d like.')
+ .appendCommentLine(' 📙 After you choose any tweak, you can download or copy to execute your script.')
+ .appendCommentLine(' 📙 Come back regularly to apply latest version for stronger privacy and security.')
+ .appendLine()
+ .appendCommentLine('-- 🧐 Why privacy.sexy')
+ .appendCommentLine(' ✔️ Rich tweak pool to harden security & privacy of the OS and other software on it.')
+ .appendCommentLine(' ✔️ No need to run any compiled software on your system, just run the generated scripts.')
+ .appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
+ .appendCommentLine(' ✔️ Open-source and free (both free as in beer and free as in speech).')
+ .toString();
+}
+
diff --git a/src/presentation/TheFooter/DownloadUrlListItem.vue b/src/presentation/TheFooter/DownloadUrlListItem.vue
index dc876d2c..646fe04e 100644
--- a/src/presentation/TheFooter/DownloadUrlListItem.vue
+++ b/src/presentation/TheFooter/DownloadUrlListItem.vue
@@ -13,11 +13,12 @@ import { Component, Prop, Watch } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment';
import { OperatingSystem } from '@/domain/OperatingSystem';
+import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
+import { IApplication } from '@/domain/IApplication';
@Component
export default class DownloadUrlListItem extends StatefulVue {
@Prop() public operatingSystem!: OperatingSystem;
- public OperatingSystem = OperatingSystem;
public downloadUrl: string = '';
public operatingSystemName: string = '';
@@ -37,6 +38,13 @@ export default class DownloadUrlListItem extends StatefulVue {
this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs);
}
+ protected initialize(app: IApplication): void {
+ return;
+ }
+ protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
+ return;
+ }
+
private async getDownloadUrlAsync(os: OperatingSystem): Promise {
const context = await this.getCurrentContextAsync();
return context.app.info.getDownloadUrl(os);
diff --git a/src/presentation/TheFooter/PrivacyPolicy.vue b/src/presentation/TheFooter/PrivacyPolicy.vue
index 087887c2..dbc17039 100644
--- a/src/presentation/TheFooter/PrivacyPolicy.vue
+++ b/src/presentation/TheFooter/PrivacyPolicy.vue
@@ -34,24 +34,22 @@
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment';
+import { IApplication } from '@/domain/IApplication';
@Component
-export default class TheFooter extends StatefulVue {
+export default class PrivacyPolicy extends StatefulVue {
public repositoryUrl: string = '';
public feedbackUrl: string = '';
- public isDesktop: boolean = false;
+ public isDesktop = Environment.CurrentEnvironment.isDesktop;
- constructor() {
- super();
- this.isDesktop = Environment.CurrentEnvironment.isDesktop;
- }
-
- public async mounted() {
- const context = await this.getCurrentContextAsync();
- const info = context.app.info;
+ protected initialize(app: IApplication): void {
+ const info = app.info;
this.repositoryUrl = info.repositoryWebUrl;
this.feedbackUrl = info.feedbackUrl;
}
+ protected handleCollectionState(): void {
+ return;
+ }
}
diff --git a/src/presentation/TheFooter/TheFooter.vue b/src/presentation/TheFooter/TheFooter.vue
index 0b7e3581..b540e38e 100644
--- a/src/presentation/TheFooter/TheFooter.vue
+++ b/src/presentation/TheFooter/TheFooter.vue
@@ -52,6 +52,8 @@ import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment';
import PrivacyPolicy from './PrivacyPolicy.vue';
import DownloadUrlList from './DownloadUrlList.vue';
+import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
+import { IApplication } from '@/domain/IApplication';
@Component({
components: {
@@ -73,15 +75,18 @@ export default class TheFooter extends StatefulVue {
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
}
- public async mounted() {
- const context = await this.getCurrentContextAsync();
- const info = context.app.info;
+ protected initialize(app: IApplication): void {
+ const info = app.info;
this.version = info.version;
this.homepageUrl = info.homepage;
this.repositoryUrl = info.repositoryWebUrl;
this.releaseUrl = info.releaseUrl;
this.feedbackUrl = info.feedbackUrl;
}
+
+ protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
+ return;
+ }
}
diff --git a/src/presentation/TheHeader.vue b/src/presentation/TheHeader.vue
index 5c3be536..3dc0a1f1 100644
--- a/src/presentation/TheHeader.vue
+++ b/src/presentation/TheHeader.vue
@@ -1,11 +1,13 @@
{{ title }}
- Enforce privacy & security on Windows
+ Enforce privacy & security on Windows and macOS
diff --git a/src/presentation/TheSearchBar.vue b/src/presentation/TheSearchBar.vue
index 4afd198a..e553669f 100644
--- a/src/presentation/TheSearchBar.vue
+++ b/src/presentation/TheSearchBar.vue
@@ -13,7 +13,11 @@
import { Component, Watch } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue';
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
-import { IUserFilter } from '@/application/Context/State/ICategoryCollectionState';
+import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
+import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
+import { IApplication } from '@/domain/IApplication';
+import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
+import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
@Component( {
directives: { NonCollapsing },
@@ -23,12 +27,7 @@ export default class TheSearchBar extends StatefulVue {
public searchPlaceHolder = 'Search';
public searchQuery = '';
- public async mounted() {
- const context = await this.getCurrentContextAsync();
- const totalScripts = context.collection.totalScripts;
- this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
- this.beginReacting(context.state.filter);
- }
+ private readonly listeners = new Array();
@Watch('searchQuery')
public async updateFilterAsync(newFilter: |string) {
@@ -40,10 +39,34 @@ export default class TheSearchBar extends StatefulVue {
filter.setFilter(newFilter);
}
}
+ public destroyed() {
+ this.unsubscribeAll();
+ }
- private beginReacting(filter: IUserFilter) {
- filter.filtered.on((result) => this.searchQuery = result.query);
- filter.filterRemoved.on(() => this.searchQuery = '');
+ protected initialize(app: IApplication): void {
+ return;
+ }
+ protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined) {
+ const totalScripts = newState.collection.totalScripts;
+ this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
+ this.searchQuery = newState.filter.currentFilter ? newState.filter.currentFilter.query : '';
+ this.unsubscribeAll();
+ this.subscribe(newState.filter);
+ }
+
+ private subscribe(filter: IUserFilter) {
+ this.listeners.push(filter.filtered.on((result) => this.handleFiltered(result)));
+ this.listeners.push(filter.filterRemoved.on(() => this.handleFilterRemoved()));
+ }
+ private unsubscribeAll() {
+ this.listeners.forEach((listener) => listener.unsubscribe());
+ this.listeners.splice(0, this.listeners.length);
+ }
+ private handleFiltered(result: IFilterResult) {
+ this.searchQuery = result.query;
+ }
+ private handleFilterRemoved() {
+ this.searchQuery = '';
}
}
diff --git a/tests/unit/application/Context/ApplicationContext.spec.ts b/tests/unit/application/Context/ApplicationContext.spec.ts
index 8934b9f0..bf873b19 100644
--- a/tests/unit/application/Context/ApplicationContext.spec.ts
+++ b/tests/unit/application/Context/ApplicationContext.spec.ts
@@ -22,7 +22,7 @@ describe('ApplicationContext', () => {
.construct();
sut.changeContext(OperatingSystem.macOS);
// assert
- expect(sut.collection).to.equal(expectedCollection);
+ expect(sut.state.collection).to.equal(expectedCollection);
});
it('currentOs is changed as expected', () => {
// arrange
@@ -35,9 +35,9 @@ describe('ApplicationContext', () => {
.construct();
sut.changeContext(expectedOs);
// assert
- expect(sut.currentOs).to.equal(expectedOs);
+ expect(sut.state.os).to.equal(expectedOs);
});
- it('state is changed as expected', () => {
+ it('new state is empty', () => {
// arrange
const testContext = new ObservableApplicationContextFactory()
.withAppContainingCollections(OperatingSystem.Windows, OperatingSystem.macOS);
@@ -45,6 +45,7 @@ describe('ApplicationContext', () => {
const sut = testContext
.withInitialOs(OperatingSystem.Windows)
.construct();
+ sut.state.filter.setFilter('filtered');
sut.changeContext(OperatingSystem.macOS);
// assert
expectEmptyState(sut.state);
@@ -82,12 +83,13 @@ describe('ApplicationContext', () => {
const sut = testContext
.withInitialOs(OperatingSystem.Windows)
.construct();
+ const oldState = sut.state;
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);
+ expect(testContext.firedEvents[0].newState.collection).to.equal(expectedCollection);
+ expect(testContext.firedEvents[0].oldState).to.equal(oldState);
});
it('is not fired when initial os is changed to same one', () => {
// arrange
@@ -148,7 +150,7 @@ describe('ApplicationContext', () => {
.withInitialOs(os)
.construct();
// assert
- const actual = sut.collection;
+ const actual = sut.state.collection;
expect(actual).to.deep.equal(expected);
});
});
@@ -174,7 +176,7 @@ describe('ApplicationContext', () => {
.withInitialOs(expected)
.construct();
// assert
- const actual = sut.currentOs;
+ const actual = sut.state.os;
expect(actual).to.deep.equal(expected);
});
describe('throws when OS is invalid', () => {
diff --git a/tests/unit/application/Context/ApplicationContextProvider.spec.ts b/tests/unit/application/Context/ApplicationContextProvider.spec.ts
index 894a66e6..1b9fddfa 100644
--- a/tests/unit/application/Context/ApplicationContextProvider.spec.ts
+++ b/tests/unit/application/Context/ApplicationContextProvider.spec.ts
@@ -17,7 +17,7 @@ describe('ApplicationContextProvider', () => {
// act
const context = buildContext(parserMock);
// assert
- // TODO: expect(expected).to.equal(context.app);
+ expect(expected).to.equal(context.app);
});
describe('sets initial OS as expected', () => {
it('returns currentOs if it is supported', () => {
@@ -28,7 +28,8 @@ describe('ApplicationContextProvider', () => {
// act
const context = buildContext(parser, environment);
// assert
- expect(expected).to.equal(context.currentOs);
+ const actual = context.state.os;
+ expect(expected).to.equal(actual);
});
it('fallbacks to other os if OS in environment is not supported', () => {
// arrange
@@ -39,11 +40,25 @@ describe('ApplicationContextProvider', () => {
// act
const context = buildContext(parser, environment);
// assert
- const actual = context.currentOs;
+ const actual = context.state.os;
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
+ // arrange
+ const expectedOs = OperatingSystem.Android;
+ const allCollections = [
+ new CategoryCollectionStub().withOs(OperatingSystem.Linux).withTotalScripts(3),
+ new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5),
+ new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4),
+ ];
+ const environment = new EnvironmentStub().withOs(OperatingSystem.macOS);
+ const app = new ApplicationStub().withCollections(...allCollections);
+ const parser: ApplicationParserType = () => app;
+ // act
+ const context = buildContext(parser, environment);
+ // assert
+ const actual = context.state.os;
+ expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
});
});
});
diff --git a/tests/unit/application/Context/State/ApplicationState.spec.ts b/tests/unit/application/Context/State/CategoryCollectionState.spec.ts
similarity index 82%
rename from tests/unit/application/Context/State/ApplicationState.spec.ts
rename to tests/unit/application/Context/State/CategoryCollectionState.spec.ts
index fc6cec40..2c35b59b 100644
--- a/tests/unit/application/Context/State/ApplicationState.spec.ts
+++ b/tests/unit/application/Context/State/CategoryCollectionState.spec.ts
@@ -3,6 +3,7 @@ import { expect } from 'chai';
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
import { ApplicationCode } from '@/application/Context/State/Code/ApplicationCode';
import { CategoryCollectionState } from '@/application/Context/State/CategoryCollectionState';
+import { OperatingSystem } from '@/domain/OperatingSystem';
import { IScript } from '@/domain/IScript';
import { ScriptStub } from '../../../stubs/ScriptStub';
import { CategoryStub } from '../../../stubs/CategoryStub';
@@ -21,7 +22,8 @@ describe('CategoryCollectionState', () => {
});
it('reacts to selection changes as expected', () => {
// arrange
- const collection = new CategoryCollectionStub().withAction(new CategoryStub(0).withScriptIds('scriptId'));
+ const collection = new CategoryCollectionStub()
+ .withAction(new CategoryStub(0).withScriptIds('scriptId'));
const selectionStub = new UserSelection(collection, []);
const expectedCodeGenerator = new ApplicationCode(selectionStub, collection.scripting);
selectionStub.selectAll();
@@ -34,6 +36,19 @@ describe('CategoryCollectionState', () => {
expect(actualCode).to.equal(expectedCode);
});
});
+ describe('os', () => {
+ it('same as its collection', () => {
+ // arrange
+ const expected = OperatingSystem.macOS;
+ const collection = new CategoryCollectionStub()
+ .withOs(expected);
+ // act
+ const sut = new CategoryCollectionState(collection);
+ // assert
+ const actual = sut.os;
+ expect(expected).to.equal(actual);
+ });
+ });
describe('selection', () => {
it('initialized with no selection', () => {
// arrange
@@ -70,7 +85,8 @@ describe('CategoryCollectionState', () => {
it('can match a script from current collection', () => {
// arrange
const scriptNameFilter = 'scriptName';
- const expectedScript = new ScriptStub('scriptId').withName(scriptNameFilter);
+ const expectedScript = new ScriptStub('scriptId')
+ .withName(scriptNameFilter);
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScript(expectedScript));
const sut = new CategoryCollectionState(collection);
diff --git a/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts b/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts
index 5fbd20b3..b8acda15 100644
--- a/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts
+++ b/tests/unit/application/Context/State/Code/Generation/CodeBuilder.spec.ts
@@ -3,10 +3,23 @@ import { expect } from 'chai';
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
describe('CodeBuilder', () => {
+ class CodeBuilderConcrete extends CodeBuilder {
+ private commentDelimiter = '//';
+ public withCommentDelimiter(delimiter: string): CodeBuilderConcrete {
+ this.commentDelimiter = delimiter;
+ return this;
+ }
+ protected getCommentDelimiter(): string {
+ return this.commentDelimiter;
+ }
+ protected writeStandardOut(text: string): string {
+ return text;
+ }
+ }
describe('appendLine', () => {
it('when empty appends empty line', () => {
// arrange
- const sut = new CodeBuilder();
+ const sut = new CodeBuilderConcrete();
// act
sut.appendLine().appendLine().appendLine();
// assert
@@ -14,7 +27,7 @@ describe('CodeBuilder', () => {
});
it('when not empty append string in new line', () => {
// arrange
- const sut = new CodeBuilder();
+ const sut = new CodeBuilderConcrete();
const expected = 'str';
// act
sut.appendLine()
@@ -27,7 +40,7 @@ describe('CodeBuilder', () => {
});
it('appendFunction', () => {
// arrange
- const sut = new CodeBuilder();
+ const sut = new CodeBuilderConcrete();
const functionName = 'function';
const code = 'code';
// act
@@ -39,11 +52,13 @@ describe('CodeBuilder', () => {
});
it('appendTrailingHyphensCommentLine', () => {
// arrange
- const sut = new CodeBuilder();
- const totalHypens = 5;
- const expected = `:: ${'-'.repeat(totalHypens)}`;
+ const commentDelimiter = '//';
+ const sut = new CodeBuilderConcrete()
+ .withCommentDelimiter(commentDelimiter);
+ const totalHyphens = 5;
+ const expected = `${commentDelimiter} ${'-'.repeat(totalHyphens)}`;
// act
- sut.appendTrailingHyphensCommentLine(totalHypens);
+ sut.appendTrailingHyphensCommentLine(totalHyphens);
// assert
const result = sut.toString();
const lines = getLines(result);
@@ -51,38 +66,45 @@ describe('CodeBuilder', () => {
});
it('appendCommentLine', () => {
// arrange
- const sut = new CodeBuilder();
+ const commentDelimiter = '//';
+ const sut = new CodeBuilderConcrete()
+ .withCommentDelimiter(commentDelimiter);
const comment = 'comment';
- const expected = ':: comment';
+ const expected = `${commentDelimiter} comment`;
// act
- sut.appendCommentLine(comment);
+ const result = sut
+ .appendCommentLine(comment)
+ .toString();
// assert
- const result = sut.toString();
const lines = getLines(result);
expect(lines[0]).to.equal(expected);
});
it('appendCommentLineWithHyphensAround', () => {
// arrange
- const sut = new CodeBuilder();
+ const commentDelimiter = '//';
+ const sut = new CodeBuilderConcrete()
+ .withCommentDelimiter(commentDelimiter);
const sectionName = 'section';
- const totalHypens = sectionName.length + 3 * 2;
- const expected = ':: ---section---';
- sut.appendCommentLineWithHyphensAround(sectionName, totalHypens);
+ const totalHyphens = sectionName.length + 3 * 2;
+ const expected = `${commentDelimiter} ---section---`;
+ // act
+ const result = sut
+ .appendCommentLineWithHyphensAround(sectionName, totalHyphens)
+ .toString();
// assert
- const result = sut.toString();
const lines = getLines(result);
expect(lines[1]).to.equal(expected);
});
describe('currentLine', () => {
it('no lines returns zero', () => {
// arrange & act
- const sut = new CodeBuilder();
+ const sut = new CodeBuilderConcrete();
// assert
expect(sut.currentLine).to.equal(0);
});
it('single line returns one', () => {
// arrange
- const sut = new CodeBuilder();
+ const sut = new CodeBuilderConcrete();
// act
sut.appendLine();
// assert
@@ -90,15 +112,17 @@ describe('CodeBuilder', () => {
});
it('multiple lines returns as expected', () => {
// arrange
- const sut = new CodeBuilder();
+ const sut = new CodeBuilderConcrete();
// act
- sut.appendLine('1').appendCommentLine('2').appendLine();
+ sut.appendLine('1')
+ .appendCommentLine('2')
+ .appendLine();
// assert
expect(sut.currentLine).to.equal(3);
});
it('multiple lines in code', () => {
// arrange
- const sut = new CodeBuilder();
+ const sut = new CodeBuilderConcrete();
// act
sut.appendLine('hello\ncode-here\nwith-3-lines');
// assert
diff --git a/tests/unit/application/Context/State/Code/Generation/CodeBuilderFactory.spec.ts b/tests/unit/application/Context/State/Code/Generation/CodeBuilderFactory.spec.ts
new file mode 100644
index 00000000..cd0b0a69
--- /dev/null
+++ b/tests/unit/application/Context/State/Code/Generation/CodeBuilderFactory.spec.ts
@@ -0,0 +1,36 @@
+import 'mocha';
+import { expect } from 'chai';
+import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
+import { ShellBuilder } from '@/application/Context/State/Code/Generation/Languages/ShellBuilder';
+import { BatchBuilder } from '@/application/Context/State/Code/Generation/Languages/BatchBuilder';
+import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
+
+describe('CodeBuilderFactory', () => {
+ describe('create', () => {
+ describe('creates expected type', () => {
+ // arrange
+ const testCases: Array< { language: ScriptingLanguage, expected: any} > = [
+ { language: ScriptingLanguage.shellscript, expected: ShellBuilder},
+ { language: ScriptingLanguage.batchfile, expected: BatchBuilder},
+ ];
+ for (const testCase of testCases) {
+ it(ScriptingLanguage[testCase.language], () => {
+ // act
+ const sut = new CodeBuilderFactory();
+ const result = sut.create(testCase.language);
+ // assert
+ expect(result).to.be.instanceOf(testCase.expected,
+ `Actual was: ${result.constructor.name}`);
+ });
+ }
+ });
+ it('throws on unknown scripting language', () => {
+ // arrange
+ const sut = new CodeBuilderFactory();
+ // act
+ const act = () => sut.create(3131313131);
+ // assert
+ expect(act).to.throw(`unknown language: "${ScriptingLanguage[3131313131]}"`);
+ });
+ });
+});
diff --git a/tests/unit/application/Context/State/Code/Generation/Languages/BatchBuilder.spec.ts b/tests/unit/application/Context/State/Code/Generation/Languages/BatchBuilder.spec.ts
new file mode 100644
index 00000000..394a42d7
--- /dev/null
+++ b/tests/unit/application/Context/State/Code/Generation/Languages/BatchBuilder.spec.ts
@@ -0,0 +1,37 @@
+import 'mocha';
+import { expect } from 'chai';
+import { BatchBuilder } from '@/application/Context/State/Code/Generation/Languages/BatchBuilder';
+
+describe('BatchBuilder', () => {
+ class BatchBuilderRevealer extends BatchBuilder {
+ public getCommentDelimiter(): string {
+ return super.getCommentDelimiter();
+ }
+ public writeStandardOut(text: string): string {
+ return super.writeStandardOut(text);
+ }
+ }
+ describe('getCommentDelimiter', () => {
+ it('returns expected', () => {
+ // arrange
+ const expected = '::';
+ const sut = new BatchBuilderRevealer();
+ // act
+ const actual = sut.getCommentDelimiter();
+ // assert
+ expect(expected).to.equal(actual);
+ });
+ });
+ describe('writeStandardOut', () => {
+ it('prepends expected', () => {
+ // arrange
+ const text = 'test';
+ const expected = `echo ${text}`;
+ const sut = new BatchBuilderRevealer();
+ // act
+ const actual = sut.writeStandardOut(text);
+ // assert
+ expect(expected).to.equal(actual);
+ });
+ });
+});
diff --git a/tests/unit/application/Context/State/Code/Generation/Languages/ShellBuilder.spec.ts b/tests/unit/application/Context/State/Code/Generation/Languages/ShellBuilder.spec.ts
new file mode 100644
index 00000000..18929a9e
--- /dev/null
+++ b/tests/unit/application/Context/State/Code/Generation/Languages/ShellBuilder.spec.ts
@@ -0,0 +1,37 @@
+import 'mocha';
+import { expect } from 'chai';
+import { ShellBuilder } from '@/application/Context/State/Code/Generation/Languages/ShellBuilder';
+
+describe('ShellBuilder', () => {
+ class ShellBuilderRevealer extends ShellBuilder {
+ public getCommentDelimiter(): string {
+ return super.getCommentDelimiter();
+ }
+ public writeStandardOut(text: string): string {
+ return super.writeStandardOut(text);
+ }
+ }
+ describe('getCommentDelimiter', () => {
+ it('returns expected', () => {
+ // arrange
+ const expected = '#';
+ const sut = new ShellBuilderRevealer();
+ // act
+ const actual = sut.getCommentDelimiter();
+ // assert
+ expect(expected).to.equal(actual);
+ });
+ });
+ describe('writeStandardOut', () => {
+ it('prepends expected', () => {
+ // arrange
+ const text = 'test';
+ const expected = `echo '${text}'`;
+ const sut = new ShellBuilderRevealer();
+ // act
+ const actual = sut.writeStandardOut(text);
+ // assert
+ expect(expected).to.equal(actual);
+ });
+ });
+});
diff --git a/tests/unit/application/Context/State/Code/Generation/UserScriptGenerator.spec.ts b/tests/unit/application/Context/State/Code/Generation/UserScriptGenerator.spec.ts
index dd2fdd54..25032a66 100644
--- a/tests/unit/application/Context/State/Code/Generation/UserScriptGenerator.spec.ts
+++ b/tests/unit/application/Context/State/Code/Generation/UserScriptGenerator.spec.ts
@@ -2,7 +2,8 @@ import 'mocha';
import { expect } from 'chai';
import { UserScriptGenerator } from '@/application/Context/State/Code/Generation/UserScriptGenerator';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
-import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
+import { ICodeBuilderFactory } from '@/application/Context/State/Code/Generation/ICodeBuilderFactory';
+import { ICodeBuilder } from '@/application/Context/State/Code/Generation/ICodeBuilder';
import { ScriptStub } from '../../../../../stubs/ScriptStub';
import { ScriptingDefinitionStub } from '../../../../../stubs/ScriptingDefinitionStub';
@@ -28,14 +29,15 @@ describe('UserScriptGenerator', () => {
});
it('is not prepended if empty', () => {
// arrange
- const sut = new UserScriptGenerator();
+ const codeBuilderStub = new CodeBuilderStub();
+ const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub));
const script = new ScriptStub('id')
.withCode('code\nmulti-lined')
.toSelectedScript();
const definition = new ScriptingDefinitionStub()
.withStartCode(undefined)
.withEndCode(undefined);
- const expectedStart = new CodeBuilder()
+ const expectedStart = codeBuilderStub
.appendFunction(script.script.name, script.script.code.execute)
.toString();
// act
@@ -64,15 +66,16 @@ describe('UserScriptGenerator', () => {
});
it('is not appended if empty', () => {
// arrange
- const sut = new UserScriptGenerator();
+ const codeBuilderStub = new CodeBuilderStub();
+ const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub));
const script = new ScriptStub('id')
.withCode('code\nmulti-lined')
.toSelectedScript();
- const definition = new ScriptingDefinitionStub()
- .withEndCode(undefined);
- const expectedEnd = new CodeBuilder()
+ const expectedEnd = codeBuilderStub
.appendFunction(script.script.name, script.script.code.execute)
.toString();
+ const definition = new ScriptingDefinitionStub()
+ .withEndCode(undefined);
// act
const code = sut.buildCode([script], definition);
// assert
@@ -199,3 +202,36 @@ describe('UserScriptGenerator', () => {
});
});
});
+
+function mockCodeBuilderFactory(mock: ICodeBuilder): ICodeBuilderFactory {
+ return {
+ create: () => mock,
+ };
+}
+
+class CodeBuilderStub implements ICodeBuilder {
+ public currentLine = 0;
+ private text = '';
+ public appendLine(code?: string): ICodeBuilder {
+ this.text += this.text ? `${code}\n` : code;
+ this.currentLine++;
+ return this;
+ }
+ public appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder {
+ return this.appendLine(`trailing-hyphens-${totalRepeatHyphens}`);
+ }
+ public appendCommentLine(commentLine?: string): ICodeBuilder {
+ return this.appendLine(`Comment | ${commentLine}`);
+ }
+ public appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder {
+ return this.appendLine(`hyphens-around-${totalRepeatHyphens} | Section name: ${sectionName} | hyphens-around-${totalRepeatHyphens}`);
+ }
+ public appendFunction(name: string, code: string): ICodeBuilder {
+ return this
+ .appendLine(`Function | Name: ${name}`)
+ .appendLine(`Function | Code: ${code}`);
+ }
+ public toString(): string {
+ return this.text;
+ }
+}
diff --git a/tests/unit/application/Parser/ApplicationParser.spec.ts b/tests/unit/application/Parser/ApplicationParser.spec.ts
index 69a486d8..2eecc311 100644
--- a/tests/unit/application/Parser/ApplicationParser.spec.ts
+++ b/tests/unit/application/Parser/ApplicationParser.spec.ts
@@ -3,11 +3,13 @@ import { expect } from 'chai';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser';
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
+import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
import { CollectionData } from 'js-yaml-loader!@/*';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { ProjectInformation } from '@/domain/ProjectInformation';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
+import { getEnumValues } from '@/application/Common/Enum';
import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub';
import { getProcessEnvironmentStub } from '../../stubs/ProcessEnvironmentStub';
import { CollectionDataStub } from '../../stubs/CollectionDataStub';
@@ -24,15 +26,18 @@ describe('ApplicationParser', () => {
it('returns result from the parser', () => {
// arrange
const os = OperatingSystem.macOS;
+ const data = new CollectionDataStub();
const expected = new CategoryCollectionStub()
.withOs(os);
const parser = new CategoryCollectionParserSpy()
- .setResult(expected)
+ .setUpReturnValue(data, expected)
.mockParser();
+ const env = getProcessEnvironmentStub();
+ const collections = [ data ];
// act
- const context = parseApplication(parser);
+ const app = parseApplication(parser, env, collections);
// assert
- const actual = context.getCollection(os);
+ const actual = app.getCollection(os);
expect(expected).to.equal(actual);
});
});
@@ -44,10 +49,10 @@ describe('ApplicationParser', () => {
const parserSpy = new CategoryCollectionParserSpy();
const parserMock = parserSpy.mockParser();
// act
- const context = parseApplication(parserMock, env);
+ const app = parseApplication(parserMock, env);
// assert
- expect(expected).to.deep.equal(context.info);
- expect(expected).to.deep.equal(parserSpy.lastArguments.info);
+ expect(expected).to.deep.equal(app.info);
+ expect(parserSpy.arguments.map((arg) => arg.info).every((info) => info === expected));
});
it('defaults to process.env', () => {
// arrange
@@ -56,54 +61,110 @@ describe('ApplicationParser', () => {
const parserSpy = new CategoryCollectionParserSpy();
const parserMock = parserSpy.mockParser();
// act
- const context = parseApplication(parserMock);
+ const app = parseApplication(parserMock);
// assert
- expect(expected).to.deep.equal(context.info);
- expect(expected).to.deep.equal(parserSpy.lastArguments.info);
+ expect(expected).to.deep.equal(app.info);
+ expect(parserSpy.arguments.map((arg) => arg.info).every((info) => info === expected));
});
});
- describe('collectionData', () => {
- it('parsed with expected data', () => {
+ describe('collectionsData', () => {
+ describe('set as expected', () => {
// arrange
- const expected = new CollectionDataStub();
- const env = getProcessEnvironmentStub();
- const parserSpy = new CategoryCollectionParserSpy();
- const parserMock = parserSpy.mockParser();
+ const testCases = [
+ {
+ name: 'single collection',
+ input: [ new CollectionDataStub() ],
+ output: [ new CategoryCollectionStub().withOs(OperatingSystem.macOS) ],
+ },
+ {
+ name: 'multiple collections',
+ input: [
+ new CollectionDataStub().withOs('windows'),
+ new CollectionDataStub().withOs('macos'),
+ ],
+ output: [
+ new CategoryCollectionStub().withOs(OperatingSystem.macOS),
+ new CategoryCollectionStub().withOs(OperatingSystem.Windows),
+ ],
+ },
+ ];
// act
- parseApplication(parserMock, env, expected);
- // assert
- expect(expected).to.equal(parserSpy.lastArguments.file);
+ for (const testCase of testCases) {
+ it(testCase.name, () => {
+ const env = getProcessEnvironmentStub();
+ let parserSpy = new CategoryCollectionParserSpy();
+ for (let i = 0; i < testCase.input.length; i++) {
+ parserSpy = parserSpy.setUpReturnValue(testCase.input[i], testCase.output[i]);
+ }
+ const parserMock = parserSpy.mockParser();
+ // act
+ const app = parseApplication(parserMock, env, testCase.input);
+ // assert
+ expect(app.collections).to.deep.equal(testCase.output);
+ });
+ }
});
- it('defaults to windows data', () => {
+ it('defaults to expected data', () => {
// arrange
- const expected = WindowsData;
+ const expected = [ WindowsData, MacOsData ];
const parserSpy = new CategoryCollectionParserSpy();
const parserMock = parserSpy.mockParser();
// act
parseApplication(parserMock);
// assert
- expect(expected).to.equal(parserSpy.lastArguments.file);
+ const actual = parserSpy.arguments.map((args) => args.data);
+ expect(actual).to.deep.equal(expected);
+ });
+ describe('throws when data is invalid', () => {
+ // arrange
+ const testCases = [
+ {
+ expectedError: 'no collection provided',
+ data: [],
+ },
+ {
+ expectedError: 'undefined collection provided',
+ data: [ new CollectionDataStub(), undefined ],
+ },
+ ];
+ for (const testCase of testCases) {
+ it(testCase.expectedError, () => {
+ const parserMock = new CategoryCollectionParserSpy().mockParser();
+ const env = getProcessEnvironmentStub();
+ // act
+ const act = () => parseApplication(parserMock, env, testCase.data);
+ // assert
+ expect(act).to.throw(testCase.expectedError);
+ });
+ }
});
});
});
});
class CategoryCollectionParserSpy {
- public lastArguments: {
- file: CollectionData;
- info: ProjectInformation;
- } = { file: undefined, info: undefined };
- private result: ICategoryCollection = new CategoryCollectionStub();
+ public arguments = new Array<{
+ data: CollectionData,
+ info: ProjectInformation,
+ }>();
- public setResult(collection: ICategoryCollection): CategoryCollectionParserSpy {
- this.result = collection;
+ private returnValues = new Map();
+
+ public setUpReturnValue(data: CollectionData, collection: ICategoryCollection): CategoryCollectionParserSpy {
+ this.returnValues.set(data, collection);
return this;
}
public mockParser(): CategoryCollectionParserType {
- return (file: CollectionData, info: IProjectInformation) => {
- this.lastArguments.file = file;
- this.lastArguments.info = info;
- return this.result;
+ return (data: CollectionData, info: IProjectInformation) => {
+ this.arguments.push({ data, info });
+ if (this.returnValues.has(data)) {
+ return this.returnValues.get(data);
+ } else {
+ // Get next OS with a unique OS so mock does not result in invalid app (with duplicate OS collections)
+ const currentRun = this.arguments.length - 1;
+ const nextOs = getEnumValues(OperatingSystem)[currentRun];
+ return new CategoryCollectionStub().withOs(nextOs);
+ }
};
}
}
diff --git a/tests/unit/application/Parser/CategoryCollectionParser.spec.ts b/tests/unit/application/Parser/CategoryCollectionParser.spec.ts
index a61df8ac..e2c8b555 100644
--- a/tests/unit/application/Parser/CategoryCollectionParser.spec.ts
+++ b/tests/unit/application/Parser/CategoryCollectionParser.spec.ts
@@ -8,8 +8,8 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinitionParser';
import { mockEnumParser } from '../../stubs/EnumParserStub';
import { ProjectInformationStub } from '../../stubs/ProjectInformationStub';
-import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub';
+import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
describe('CategoryCollectionParser', () => {
describe('parseCategoryCollection', () => {
@@ -48,8 +48,8 @@ describe('CategoryCollectionParser', () => {
it('parses actions', () => {
// arrange
const actions = [ getCategoryStub('test1'), getCategoryStub('test2') ];
- const compiler = new ScriptCompilerStub();
- const expected = [ parseCategory(actions[0], compiler), parseCategory(actions[1], compiler) ];
+ const context = new CategoryCollectionParseContextStub();
+ const expected = [ parseCategory(actions[0], context), parseCategory(actions[1], context) ];
const collection = new CollectionDataStub()
.withActions(actions);
const info = new ProjectInformationStub();
diff --git a/tests/unit/application/Parser/CategoryParser.spec.ts b/tests/unit/application/Parser/CategoryParser.spec.ts
index 9e08ab9c..b60a336d 100644
--- a/tests/unit/application/Parser/CategoryParser.spec.ts
+++ b/tests/unit/application/Parser/CategoryParser.spec.ts
@@ -2,10 +2,12 @@ import 'mocha';
import { expect } from 'chai';
import { parseCategory } from '@/application/Parser/CategoryParser';
import { CategoryData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
-import { parseScript } from '@/application/Parser/ScriptParser';
+import { parseScript } from '@/application/Parser/Script/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
+import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
+import { LanguageSyntaxStub } from '../../stubs/LanguageSyntaxStub';
describe('CategoryParser', () => {
describe('parseCategory', () => {
@@ -14,9 +16,9 @@ describe('CategoryParser', () => {
// arrange
const expectedMessage = 'category is null or undefined';
const category = undefined;
- const compiler = new ScriptCompilerStub();
+ const context = new CategoryCollectionParseContextStub();
// act
- const act = () => parseCategory(category, compiler);
+ const act = () => parseCategory(category, context);
// assert
expect(act).to.throw(expectedMessage);
});
@@ -28,9 +30,9 @@ describe('CategoryParser', () => {
category: categoryName,
children: [],
};
- const compiler = new ScriptCompilerStub();
+ const context = new CategoryCollectionParseContextStub();
// act
- const act = () => parseCategory(category, compiler);
+ const act = () => parseCategory(category, context);
// assert
expect(act).to.throw(expectedMessage);
});
@@ -42,9 +44,9 @@ describe('CategoryParser', () => {
category: categoryName,
children: undefined,
};
- const compiler = new ScriptCompilerStub();
+ const context = new CategoryCollectionParseContextStub();
// act
- const act = () => parseCategory(category, compiler);
+ const act = () => parseCategory(category, context);
// assert
expect(act).to.throw(expectedMessage);
});
@@ -57,21 +59,21 @@ describe('CategoryParser', () => {
category: invalidName,
children: getTestChildren(),
};
- const compiler = new ScriptCompilerStub();
+ const context = new CategoryCollectionParseContextStub();
// act
- const act = () => parseCategory(category, compiler);
+ const act = () => parseCategory(category, context);
// assert
expect(act).to.throw(expectedMessage);
});
});
});
- it('throws when compiler is undefined', () => {
+ it('throws when context is undefined', () => {
// arrange
- const expectedError = 'undefined compiler';
- const compiler = undefined;
+ const expectedError = 'undefined context';
+ const context = undefined;
const category = getValidCategory();
// act
- const act = () => parseCategory(category, compiler);
+ const act = () => parseCategory(category, context);
// assert
expect(act).to.throw(expectedError);
});
@@ -79,14 +81,14 @@ describe('CategoryParser', () => {
// arrange
const url = 'https://privacy.sexy';
const expected = parseDocUrls({ docs: url });
- const compiler = new ScriptCompilerStub();
const category: CategoryData = {
category: 'category name',
children: getTestChildren(),
docs: url,
};
+ const context = new CategoryCollectionParseContextStub();
// act
- const actual = parseCategory(category, compiler).documentationUrls;
+ const actual = parseCategory(category, context).documentationUrls;
// assert
expect(actual).to.deep.equal(expected);
});
@@ -94,14 +96,14 @@ describe('CategoryParser', () => {
it('single script with code', () => {
// arrange
const script = ScriptDataStub.createWithCode();
- const compiler = new ScriptCompilerStub();
- const expected = [ parseScript(script, compiler) ];
+ const context = new CategoryCollectionParseContextStub();
+ const expected = [ parseScript(script, context) ];
const category: CategoryData = {
category: 'category name',
children: [ script ],
};
// act
- const actual = parseCategory(category, compiler).scripts;
+ const actual = parseCategory(category, context).scripts;
// assert
expect(actual).to.deep.equal(expected);
});
@@ -110,13 +112,15 @@ describe('CategoryParser', () => {
const script = ScriptDataStub.createWithCall();
const compiler = new ScriptCompilerStub()
.withCompileAbility(script);
- const expected = [ parseScript(script, compiler) ];
+ const context = new CategoryCollectionParseContextStub()
+ .withCompiler(compiler);
+ const expected = [ parseScript(script, context) ];
const category: CategoryData = {
category: 'category name',
children: [ script ],
};
// act
- const actual = parseCategory(category, compiler).scripts;
+ const actual = parseCategory(category, context).scripts;
// assert
expect(actual).to.deep.equal(expected);
});
@@ -124,18 +128,44 @@ describe('CategoryParser', () => {
// arrange
const callableScript = ScriptDataStub.createWithCall();
const scripts = [ callableScript, ScriptDataStub.createWithCode() ];
- const compiler = new ScriptCompilerStub()
- .withCompileAbility(callableScript);
- const expected = scripts.map((script) => parseScript(script, compiler));
const category: CategoryData = {
category: 'category name',
children: scripts,
};
+ const compiler = new ScriptCompilerStub()
+ .withCompileAbility(callableScript);
+ const context = new CategoryCollectionParseContextStub()
+ .withCompiler(compiler);
+ const expected = scripts.map((script) => parseScript(script, context));
// act
- const actual = parseCategory(category, compiler).scripts;
+ const actual = parseCategory(category, context).scripts;
// assert
expect(actual).to.deep.equal(expected);
});
+ it('script is created with right context', () => { // test through script validation logic
+ // arrange
+ const commentDelimiter = 'should not throw';
+ const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
+ const parseContext = new CategoryCollectionParseContextStub()
+ .withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
+ const category: CategoryData = {
+ category: 'category name',
+ children: [
+ {
+ category: 'sub-category',
+ children: [
+ ScriptDataStub
+ .createWithoutCallOrCodes()
+ .withCode(duplicatedCode),
+ ],
+ },
+ ],
+ };
+ // act
+ const act = () => parseCategory(category, parseContext).scripts;
+ // assert
+ expect(act).to.not.throw();
+ });
});
it('returns expected subcategories', () => {
// arrange
@@ -147,9 +177,9 @@ describe('CategoryParser', () => {
category: 'category name',
children: expected,
};
- const compiler = new ScriptCompilerStub();
+ const context = new CategoryCollectionParseContextStub();
// act
- const actual = parseCategory(category, compiler).subCategories;
+ const actual = parseCategory(category, context).subCategories;
// assert
expect(actual).to.have.lengthOf(1);
expect(actual[0].name).to.equal(expected[0].category);
diff --git a/tests/unit/application/Parser/Compiler/ScriptCompiler.spec.ts b/tests/unit/application/Parser/Compiler/ScriptCompiler.spec.ts
deleted file mode 100644
index 485044bc..00000000
--- a/tests/unit/application/Parser/Compiler/ScriptCompiler.spec.ts
+++ /dev/null
@@ -1,325 +0,0 @@
-import 'mocha';
-import { expect } from 'chai';
-import { ScriptCompiler } from '@/application/Parser/Compiler/ScriptCompiler';
-import { ScriptDataStub } from '../../../stubs/ScriptDataStub';
-import { FunctionData, ScriptData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
-import { IScriptCode } from '@/domain/IScriptCode';
-import { IScriptCompiler } from '@/application/Parser/Compiler/IScriptCompiler';
-
-describe('ScriptCompiler', () => {
- describe('ctor', () => {
- it('throws when functions have same names', () => {
- // arrange
- const expectedError = `duplicate function name: "same-func-name"`;
- const functions: FunctionData[] = [ {
- name: 'same-func-name',
- code: 'non-empty-code',
- }, {
- name: 'same-func-name',
- code: 'non-empty-code-2',
- }];
- // act
- const act = () => new ScriptCompiler(functions);
- // assert
- expect(act).to.throw(expectedError);
- });
- it('throws when function parameters have same names', () => {
- // arrange
- const func: FunctionData = {
- name: 'function-name',
- code: 'non-empty-code',
- parameters: [ 'duplicate', 'duplicate' ],
- };
- const expectedError = `"${func.name}": duplicate parameter name: "duplicate"`;
- // act
- const act = () => new ScriptCompiler([func]);
- // assert
- expect(act).to.throw(expectedError);
- });
- describe('throws when when function have duplicate code', () => {
- it('code', () => {
- // arrange
- const expectedError = `duplicate "code" in functions: "duplicate-code"`;
- const functions: FunctionData[] = [ {
- name: 'func-1',
- code: 'duplicate-code',
- }, {
- name: 'func-2',
- code: 'duplicate-code',
- }];
- // act
- const act = () => new ScriptCompiler(functions);
- // assert
- expect(act).to.throw(expectedError);
- });
- it('revertCode', () => {
- // arrange
- const expectedError = `duplicate "revertCode" in functions: "duplicate-revert-code"`;
- const functions: FunctionData[] = [ {
- name: 'func-1',
- code: 'code-1',
- revertCode: 'duplicate-revert-code',
- }, {
- name: 'func-2',
- code: 'code-2',
- revertCode: 'duplicate-revert-code',
- }];
- // act
- const act = () => new ScriptCompiler(functions);
- // assert
- expect(act).to.throw(expectedError);
- });
- });
- });
- describe('canCompile', () => {
- it('returns true if "call" is defined', () => {
- // arrange
- const sut = new ScriptCompiler([]);
- const script = ScriptDataStub.createWithCall();
- // act
- const actual = sut.canCompile(script);
- // assert
- expect(actual).to.equal(true);
- });
- it('returns false if "call" is undefined', () => {
- // arrange
- const sut = new ScriptCompiler([]);
- const script = ScriptDataStub.createWithCode();
- // act
- const actual = sut.canCompile(script);
- // assert
- expect(actual).to.equal(false);
- });
- });
- describe('compile', () => {
- describe('invalid state', () => {
- it('throws if functions are empty', () => {
- // arrange
- const expectedError = 'cannot compile without shared functions';
- const functions = [];
- const sut = new ScriptCompiler(functions);
- const script = ScriptDataStub.createWithCall();
- // act
- const act = () => sut.compile(script);
- // assert
- expect(act).to.throw(expectedError);
- });
- it('throws if call is not an object', () => {
- // arrange
- const expectedError = 'called function(s) must be an object';
- const invalidValues = [undefined, 'string', 33];
- const sut = new ScriptCompiler(createFunctions());
- invalidValues.forEach((invalidValue) => {
- const script = ScriptDataStub.createWithoutCallOrCodes() // because call ctor overwrites "undefined"
- .withCall(invalidValue as any);
- // act
- const act = () => sut.compile(script);
- // assert
- expect(act).to.throw(expectedError);
- });
- });
- describe('invalid function reference', () => {
- it('throws if function does not exist', () => {
- // arrange
- const sut = new ScriptCompiler(createFunctions());
- const nonExistingFunctionName = 'non-existing-func';
- const expectedError = `called function is not defined "${nonExistingFunctionName}"`;
- const call: ScriptFunctionCallData = { function: nonExistingFunctionName };
- const script = ScriptDataStub.createWithCall(call);
- // act
- const act = () => sut.compile(script);
- // assert
- expect(act).to.throw(expectedError);
- });
- it('throws if function is undefined', () => {
- // arrange
- const existingFunctionName = 'existing-func';
- const sut = new ScriptCompiler(createFunctions(existingFunctionName));
- const call: ScriptFunctionCallData = [
- { function: existingFunctionName },
- undefined,
- ];
- const script = ScriptDataStub.createWithCall(call);
- const expectedError = `undefined function call in script "${script.name}"`;
- // act
- const act = () => sut.compile(script);
- // assert
- expect(act).to.throw(expectedError);
- });
- it('throws if function name is not given', () => {
- // arrange
- const existingFunctionName = 'existing-func';
- const sut = new ScriptCompiler(createFunctions(existingFunctionName));
- const call: FunctionCallData[] = [
- { function: existingFunctionName },
- { function: undefined }];
- const script = ScriptDataStub.createWithCall(call);
- const expectedError = `empty function name called in script "${script.name}"`;
- // act
- const act = () => sut.compile(script);
- // assert
- expect(act).to.throw(expectedError);
- });
- });
- });
- describe('builds code as expected', () => {
- it('builds single call as expected', () => {
- // arrange
- const functionName = 'testSharedFunction';
- const expected: IScriptCode = {
- execute: 'expected-code',
- revert: 'expected-revert-code',
- };
- const func: FunctionData = {
- name: functionName,
- parameters: [],
- code: expected.execute,
- revertCode: expected.revert,
- };
- const sut = new ScriptCompiler([func]);
- const call: FunctionCallData = { function: functionName };
- const script = ScriptDataStub.createWithCall(call);
- // act
- const actual = sut.compile(script);
- // assert
- expect(actual).to.deep.equal(expected);
- });
- it('builds call sequence as expected', () => {
- // arrange
- const firstFunction: FunctionData = {
- name: 'first-function-name',
- parameters: [],
- code: 'first-function-code',
- revertCode: 'first-function-revert-code',
- };
- const secondFunction: FunctionData = {
- name: 'second-function-name',
- parameters: [],
- code: 'second-function-code',
- revertCode: 'second-function-revert-code',
- };
- const expected: IScriptCode = {
- execute: 'first-function-code\nsecond-function-code',
- revert: 'first-function-revert-code\nsecond-function-revert-code',
- };
- const sut = new ScriptCompiler([firstFunction, secondFunction]);
- const call: FunctionCallData[] = [
- { function: firstFunction.name },
- { function: secondFunction.name },
- ];
- const script = ScriptDataStub.createWithCall(call);
- // act
- const actual = sut.compile(script);
- // assert
- expect(actual).to.deep.equal(expected);
- });
- });
- describe('parameter substitution', () => {
- describe('substitutes as expected', () => {
- it('with different parameters', () => {
- // arrange
- const env = new TestEnvironment({
- code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
- parameters: {
- firstParameter: 'llo',
- secondParameter: 'world',
- },
- });
- const expected = env.expect('Hello world!');
- // act
- const actual = env.sut.compile(env.script);
- // assert
- expect(actual).to.deep.equal(expected);
- });
- it('with single parameter', () => {
- // arrange
- const env = new TestEnvironment({
- code: '{{ $parameter }}!',
- parameters: {
- parameter: 'Hodor',
- },
- });
- const expected = env.expect('Hodor!');
- // act
- const actual = env.sut.compile(env.script);
- // assert
- expect(actual).to.deep.equal(expected);
- });
- });
- it('throws when parameters is undefined', () => {
- // arrange
- const env = new TestEnvironment({
- code: '{{ $parameter }} {{ $parameter }}!',
- });
- const expectedError = 'no parameters defined, expected: "parameter"';
- // act
- const act = () => env.sut.compile(env.script);
- // assert
- expect(act).to.throw(expectedError);
- });
- it('throws when parameter value is not provided', () => {
- // arrange
- const env = new TestEnvironment({
- code: '{{ $parameter }} {{ $parameter }}!',
- parameters: {
- parameter: undefined,
- },
- });
- const expectedError = 'parameter value is not provided for "parameter" in function call';
- // act
- const act = () => env.sut.compile(env.script);
- // assert
- expect(act).to.throw(expectedError);
- });
- });
- });
-});
-
-interface ITestCase {
- code: string;
- parameters?: FunctionCallParametersData;
-}
-
-class TestEnvironment {
- public readonly sut: IScriptCompiler;
- public readonly script: ScriptData;
- constructor(testCase: ITestCase) {
- const functionName = 'testFunction';
- const func: FunctionData = {
- name: functionName,
- parameters: testCase.parameters ? Object.keys(testCase.parameters) : undefined,
- code: this.getCode(testCase.code, 'execute'),
- revertCode: this.getCode(testCase.code, 'revert'),
- };
- this.sut = new ScriptCompiler([func]);
- const call: FunctionCallData = {
- function: functionName,
- parameters: testCase.parameters,
- };
- this.script = ScriptDataStub.createWithCall(call);
- }
- public expect(code: string): IScriptCode {
- return {
- execute: this.getCode(code, 'execute'),
- revert: this.getCode(code, 'revert'),
- };
- }
- private getCode(text: string, type: 'execute' | 'revert'): string {
- return `${text} (${type})`;
- }
-}
-
-function createFunctions(...names: string[]): FunctionData[] {
- if (!names || names.length === 0) {
- names = ['test-function'];
- }
- return names.map((functionName) => {
- const func: FunctionData = {
- name: functionName,
- parameters: [],
- code: `REM test-code (${functionName})`,
- revertCode: `REM test-revert-code (${functionName})`,
- };
- return func;
- });
-}
diff --git a/tests/unit/application/Parser/Script/CategoryCollectionParseContext.spec.ts b/tests/unit/application/Parser/Script/CategoryCollectionParseContext.spec.ts
new file mode 100644
index 00000000..86fb9ff3
--- /dev/null
+++ b/tests/unit/application/Parser/Script/CategoryCollectionParseContext.spec.ts
@@ -0,0 +1,82 @@
+import 'mocha';
+import { expect } from 'chai';
+import { ISyntaxFactory } from '@/application/Parser/Script/Syntax/ISyntaxFactory';
+import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
+import { LanguageSyntaxStub } from '../../../stubs/LanguageSyntaxStub';
+import { CategoryCollectionParseContext } from '@/application/Parser/Script/CategoryCollectionParseContext';
+import { ScriptingDefinitionStub } from '../../../stubs/ScriptingDefinitionStub';
+import { FunctionDataStub } from '../../../stubs/FunctionDataStub';
+import { ILanguageSyntax } from '@/domain/ScriptCode';
+import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
+import { FunctionData } from 'js-yaml-loader!*';
+
+describe('CategoryCollectionParseContext', () => {
+ describe('ctor', () => {
+ describe('functionsData', () => {
+ it('can create with empty values', () => {
+ // arrange
+ const testData: FunctionData[][] = [ undefined, [] ];
+ const scripting = new ScriptingDefinitionStub();
+ for (const functionsData of testData) {
+ // act
+ const act = () => new CategoryCollectionParseContext(functionsData, scripting);
+ // assert
+ expect(act).to.not.throw();
+ }
+ });
+ });
+ it('scripting', () => {
+ // arrange
+ const expectedError = 'undefined scripting';
+ const scripting = undefined;
+ const functionsData = [ new FunctionDataStub() ];
+ // act
+ const act = () => new CategoryCollectionParseContext(functionsData, scripting);
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ });
+ describe('compiler', () => {
+ it('constructed as expected', () => {
+ // arrange
+ const functionsData = [ new FunctionDataStub() ];
+ const syntax = new LanguageSyntaxStub();
+ const expected = new ScriptCompiler(functionsData, syntax);
+ const language = ScriptingLanguage.shellscript;
+ const factoryMock = mockFactory(language, syntax);
+ const definition = new ScriptingDefinitionStub()
+ .withLanguage(language);
+ // act
+ const sut = new CategoryCollectionParseContext(functionsData, definition, factoryMock);
+ const actual = sut.compiler;
+ // assert
+ expect(actual).to.deep.equal(expected);
+ });
+ });
+ describe('syntax', () => {
+ it('set from syntax factory', () => {
+ // arrange
+ const language = ScriptingLanguage.shellscript;
+ const expected = new LanguageSyntaxStub();
+ const factoryMock = mockFactory(language, expected);
+ const definition = new ScriptingDefinitionStub()
+ .withLanguage(language);
+ // act
+ const sut = new CategoryCollectionParseContext([], definition, factoryMock);
+ const actual = sut.syntax;
+ // assert
+ expect(actual).to.equal(expected);
+ });
+ });
+});
+
+function mockFactory(expectedLanguage: ScriptingLanguage, result: ILanguageSyntax): ISyntaxFactory {
+ return {
+ create: (language: ScriptingLanguage) => {
+ if (language !== expectedLanguage) {
+ throw new Error('unexpected language');
+ }
+ return result;
+ },
+ };
+}
diff --git a/tests/unit/application/Parser/Compiler/ILCode.spec.ts b/tests/unit/application/Parser/Script/Compiler/ILCode.spec.ts
similarity index 98%
rename from tests/unit/application/Parser/Compiler/ILCode.spec.ts
rename to tests/unit/application/Parser/Script/Compiler/ILCode.spec.ts
index f6c07991..ecacee4e 100644
--- a/tests/unit/application/Parser/Compiler/ILCode.spec.ts
+++ b/tests/unit/application/Parser/Script/Compiler/ILCode.spec.ts
@@ -1,6 +1,6 @@
import 'mocha';
import { expect } from 'chai';
-import { generateIlCode } from '@/application/Parser/Compiler/ILCode';
+import { generateIlCode } from '@/application/Parser/Script/Compiler/ILCode';
describe('ILCode', () => {
describe('getUniqueParameterNames', () => {
diff --git a/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts
new file mode 100644
index 00000000..6c4e298b
--- /dev/null
+++ b/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts
@@ -0,0 +1,405 @@
+import 'mocha';
+import { expect } from 'chai';
+import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler';
+import { FunctionData, ScriptData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
+import { IScriptCode } from '@/domain/IScriptCode';
+import { ILanguageSyntax } from '@/domain/ScriptCode';
+import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
+import { LanguageSyntaxStub } from '../../../../stubs/LanguageSyntaxStub';
+import { ScriptDataStub } from '../../../../stubs/ScriptDataStub';
+import { FunctionDataStub } from '../../../../stubs/FunctionDataStub';
+
+describe('ScriptCompiler', () => {
+ describe('ctor', () => {
+ it('throws if syntax is undefined', () => {
+ // arrange
+ const expectedError = `undefined syntax`;
+ // act
+ const act = () => new ScriptCompilerBuilder()
+ .withSomeFunctions()
+ .withSyntax(undefined)
+ .build();
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ it('throws if one of the functions is undefined', () => {
+ // arrange
+ const expectedError = `some functions are undefined`;
+ const functions = [ new FunctionDataStub(), undefined ];
+ // act
+ const act = () => new ScriptCompilerBuilder()
+ .withFunctions(...functions)
+ .build();
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ it('throws when functions have same names', () => {
+ // arrange
+ const name = 'same-func-name';
+ const expectedError = `duplicate function name: "${name}"`;
+ const functions = [
+ new FunctionDataStub().withName(name),
+ new FunctionDataStub().withName(name),
+ ];
+ // act
+ const act = () => new ScriptCompilerBuilder()
+ .withFunctions(...functions)
+ .build();
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ it('throws when function parameters have same names', () => {
+ // arrange
+ const parameterName = 'duplicate-parameter';
+ const func = new FunctionDataStub()
+ .withParameters(parameterName, parameterName);
+ const expectedError = `"${func.name}": duplicate parameter name: "${parameterName}"`;
+ // act
+ const act = () => new ScriptCompilerBuilder()
+ .withFunctions(func)
+ .build();
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ describe('throws when when function have duplicate code', () => {
+ it('code', () => {
+ // arrange
+ const code = 'duplicate-code';
+ const expectedError = `duplicate "code" in functions: "${code}"`;
+ const functions = [
+ new FunctionDataStub().withName('func-1').withCode(code),
+ new FunctionDataStub().withName('func-2').withCode(code),
+ ];
+ // act
+ const act = () => new ScriptCompilerBuilder()
+ .withFunctions(...functions)
+ .build();
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ it('revertCode', () => {
+ // arrange
+ const revertCode = 'duplicate-revert-code';
+ const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
+ const functions = [
+ new FunctionDataStub().withName('func-1').withCode('code-1').withRevertCode(revertCode),
+ new FunctionDataStub().withName('func-2').withCode('code-2').withRevertCode(revertCode),
+ ];
+ // act
+ const act = () => new ScriptCompilerBuilder()
+ .withFunctions(...functions)
+ .build();
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ });
+ it('can construct with empty functions', () => {
+ // arrange
+ const builder = new ScriptCompilerBuilder()
+ .withEmptyFunctions();
+ // act
+ const act = () => builder.build();
+ // assert
+ expect(act).to.not.throw();
+ });
+ });
+ describe('canCompile', () => {
+ it('returns true if "call" is defined', () => {
+ // arrange
+ const sut = new ScriptCompilerBuilder()
+ .withEmptyFunctions()
+ .build();
+ const script = ScriptDataStub.createWithCall();
+ // act
+ const actual = sut.canCompile(script);
+ // assert
+ expect(actual).to.equal(true);
+ });
+ it('returns false if "call" is undefined', () => {
+ // arrange
+ const sut = new ScriptCompilerBuilder()
+ .withEmptyFunctions()
+ .build();
+ const script = ScriptDataStub.createWithCode();
+ // act
+ const actual = sut.canCompile(script);
+ // assert
+ expect(actual).to.equal(false);
+ });
+ });
+ describe('compile', () => {
+ describe('invalid state', () => {
+ it('throws if functions are empty', () => {
+ // arrange
+ const expectedError = 'cannot compile without shared functions';
+ const sut = new ScriptCompilerBuilder()
+ .withEmptyFunctions()
+ .build();
+ const script = ScriptDataStub.createWithCall();
+ // act
+ const act = () => sut.compile(script);
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ it('throws if call is not an object', () => {
+ // arrange
+ const expectedError = 'called function(s) must be an object';
+ const invalidValues = [undefined, 'string', 33];
+ const sut = new ScriptCompilerBuilder()
+ .withSomeFunctions()
+ .build();
+ invalidValues.forEach((invalidValue) => {
+ const script = ScriptDataStub.createWithoutCallOrCodes() // because call ctor overwrites "undefined"
+ .withCall(invalidValue as any);
+ // act
+ const act = () => sut.compile(script);
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ });
+ describe('invalid function reference', () => {
+ it('throws if function does not exist', () => {
+ // arrange
+ const sut = new ScriptCompilerBuilder()
+ .withSomeFunctions()
+ .build();
+ const nonExistingFunctionName = 'non-existing-func';
+ const expectedError = `called function is not defined "${nonExistingFunctionName}"`;
+ const call: ScriptFunctionCallData = { function: nonExistingFunctionName };
+ const script = ScriptDataStub.createWithCall(call);
+ // act
+ const act = () => sut.compile(script);
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ it('throws if function is undefined', () => {
+ // arrange
+ const existingFunctionName = 'existing-func';
+ const sut = new ScriptCompilerBuilder()
+ .withFunctionNames(existingFunctionName)
+ .build();
+ const call: ScriptFunctionCallData = [
+ { function: existingFunctionName },
+ undefined,
+ ];
+ const script = ScriptDataStub.createWithCall(call);
+ const expectedError = `undefined function call in script "${script.name}"`;
+ // act
+ const act = () => sut.compile(script);
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ it('throws if function name is not given', () => {
+ // arrange
+ const existingFunctionName = 'existing-func';
+ const sut = new ScriptCompilerBuilder()
+ .withFunctionNames(existingFunctionName)
+ .build();
+ const call: FunctionCallData[] = [
+ { function: existingFunctionName },
+ { function: undefined }];
+ const script = ScriptDataStub.createWithCall(call);
+ const expectedError = `empty function name called in script "${script.name}"`;
+ // act
+ const act = () => sut.compile(script);
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ });
+ });
+ describe('builds code as expected', () => {
+ it('creates code with expected syntax', () => { // test through script validation logic
+ // act
+ const commentDelimiter = 'should not throw';
+ const syntax = new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter);
+ const func = new FunctionDataStub()
+ .withCode(`${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`);
+ const sut = new ScriptCompilerBuilder()
+ .withFunctions(func)
+ .withSyntax(syntax)
+ .build();
+ const call: FunctionCallData = { function: func.name };
+ const script = ScriptDataStub.createWithCall(call);
+ // act
+ const act = () => sut.compile(script);
+ // assert
+ expect(act).to.not.throw();
+ });
+ it('builds single call as expected', () => {
+ // arrange
+ const functionName = 'testSharedFunction';
+ const expectedExecute = `expected-execute`;
+ const expectedRevert = `expected-revert`;
+ const func = new FunctionDataStub()
+ .withName(functionName)
+ .withCode(expectedExecute)
+ .withRevertCode(expectedRevert);
+ const sut = new ScriptCompilerBuilder()
+ .withFunctions(func)
+ .build();
+ const call: FunctionCallData = { function: functionName };
+ const script = ScriptDataStub.createWithCall(call);
+ // act
+ const actual = sut.compile(script);
+ // assert
+ expect(actual.execute).to.equal(expectedExecute);
+ expect(actual.revert).to.equal(expectedRevert);
+ });
+ it('builds call sequence as expected', () => {
+ // arrange
+ const firstFunction = new FunctionDataStub()
+ .withName('first-function-name')
+ .withCode('first-function-code')
+ .withRevertCode('first-function-revert-code');
+ const secondFunction = new FunctionDataStub()
+ .withName('second-function-name')
+ .withCode('second-function-code')
+ .withRevertCode('second-function-revert-code');
+ const expectedExecute = `${firstFunction.code}\n${secondFunction.code}`;
+ const expectedRevert = `${firstFunction.revertCode}\n${secondFunction.revertCode}`;
+ const sut = new ScriptCompilerBuilder()
+ .withFunctions(firstFunction, secondFunction)
+ .build();
+ const call: FunctionCallData[] = [
+ { function: firstFunction.name },
+ { function: secondFunction.name },
+ ];
+ const script = ScriptDataStub.createWithCall(call);
+ // act
+ const actual = sut.compile(script);
+ // assert
+ expect(actual.execute).to.equal(expectedExecute);
+ expect(actual.revert).to.equal(expectedRevert);
+ });
+ });
+ describe('parameter substitution', () => {
+ describe('substitutes as expected', () => {
+ it('with different parameters', () => {
+ // arrange
+ const env = new TestEnvironment({
+ code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
+ parameters: {
+ firstParameter: 'llo',
+ secondParameter: 'world',
+ },
+ });
+ const expected = env.expect('Hello world!');
+ // act
+ const actual = env.sut.compile(env.script);
+ // assert
+ expect(actual).to.deep.equal(expected);
+ });
+ it('with single parameter', () => {
+ // arrange
+ const env = new TestEnvironment({
+ code: '{{ $parameter }}!',
+ parameters: {
+ parameter: 'Hodor',
+ },
+ });
+ const expected = env.expect('Hodor!');
+ // act
+ const actual = env.sut.compile(env.script);
+ // assert
+ expect(actual).to.deep.equal(expected);
+ });
+ });
+ it('throws when parameters is undefined', () => {
+ // arrange
+ const env = new TestEnvironment({
+ code: '{{ $parameter }} {{ $parameter }}!',
+ });
+ const expectedError = 'no parameters defined, expected: "parameter"';
+ // act
+ const act = () => env.sut.compile(env.script);
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ it('throws when parameter value is not provided', () => {
+ // arrange
+ const env = new TestEnvironment({
+ code: '{{ $parameter }} {{ $parameter }}!',
+ parameters: {
+ parameter: undefined,
+ },
+ });
+ const expectedError = 'parameter value is not provided for "parameter" in function call';
+ // act
+ const act = () => env.sut.compile(env.script);
+ // assert
+ expect(act).to.throw(expectedError);
+ });
+ });
+ interface ITestCase {
+ code: string;
+ parameters?: FunctionCallParametersData;
+ }
+ class TestEnvironment {
+ public readonly sut: IScriptCompiler;
+ public readonly script: ScriptData;
+ constructor(testCase: ITestCase) {
+ const functionName = 'testFunction';
+ const parameters = testCase.parameters ? Object.keys(testCase.parameters) : [];
+ const func = new FunctionDataStub()
+ .withName(functionName)
+ .withParameters(...parameters)
+ .withCode(this.getCode(testCase.code, 'execute'))
+ .withRevertCode(this.getCode(testCase.code, 'revert'));
+ const syntax = new LanguageSyntaxStub();
+ this.sut = new ScriptCompiler([func], syntax);
+ const call: FunctionCallData = {
+ function: functionName,
+ parameters: testCase.parameters,
+ };
+ this.script = ScriptDataStub.createWithCall(call);
+ }
+ public expect(code: string): IScriptCode {
+ return {
+ execute: this.getCode(code, 'execute'),
+ revert: this.getCode(code, 'revert'),
+ };
+ }
+ private getCode(text: string, type: 'execute' | 'revert'): string {
+ return `${text} (${type})`;
+ }
+ }
+ });
+});
+
+
+// tslint:disable-next-line:max-classes-per-file
+class ScriptCompilerBuilder {
+ private static createFunctions(...names: string[]): FunctionData[] {
+ return names.map((functionName) => {
+ return new FunctionDataStub().withName(functionName);
+ });
+ }
+ private functions: FunctionData[];
+ private syntax: ILanguageSyntax = new LanguageSyntaxStub();
+ public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder {
+ this.functions = functions;
+ return this;
+ }
+ public withSomeFunctions(): ScriptCompilerBuilder {
+ this.functions = ScriptCompilerBuilder.createFunctions('test-function');
+ return this;
+ }
+ public withFunctionNames(...functionNames: string[]): ScriptCompilerBuilder {
+ this.functions = ScriptCompilerBuilder.createFunctions(...functionNames);
+ return this;
+ }
+ public withEmptyFunctions(): ScriptCompilerBuilder {
+ this.functions = [];
+ return this;
+ }
+ public withSyntax(syntax: ILanguageSyntax): ScriptCompilerBuilder {
+ this.syntax = syntax;
+ return this;
+ }
+ public build(): ScriptCompiler {
+ if (!this.functions) {
+ throw new Error('Function behavior not defined');
+ }
+ return new ScriptCompiler(this.functions, this.syntax);
+ }
+}
diff --git a/tests/unit/application/Parser/ScriptParser.spec.ts b/tests/unit/application/Parser/Script/ScriptParser.spec.ts
similarity index 59%
rename from tests/unit/application/Parser/ScriptParser.spec.ts
rename to tests/unit/application/Parser/Script/ScriptParser.spec.ts
index 40b1f036..060a2916 100644
--- a/tests/unit/application/Parser/ScriptParser.spec.ts
+++ b/tests/unit/application/Parser/Script/ScriptParser.spec.ts
@@ -1,12 +1,15 @@
import 'mocha';
import { expect } from 'chai';
-import { parseScript } from '@/application/Parser/ScriptParser';
+import { parseScript } from '@/application/Parser/Script/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
-import { ScriptCode } from '@/domain/ScriptCode';
-import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
-import { ScriptDataStub } from '../../stubs/ScriptDataStub';
-import { mockEnumParser } from '../../stubs/EnumParserStub';
+import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
+import { ScriptCompilerStub } from '../../../stubs/ScriptCompilerStub';
+import { ScriptDataStub } from '../../../stubs/ScriptDataStub';
+import { mockEnumParser } from '../../../stubs/EnumParserStub';
+import { ScriptCodeStub } from '../../../stubs/ScriptCodeStub';
+import { CategoryCollectionParseContextStub } from '../../../stubs/CategoryCollectionParseContextStub';
+import { LanguageSyntaxStub } from '../../../stubs/LanguageSyntaxStub';
describe('ScriptParser', () => {
describe('parseScript', () => {
@@ -15,9 +18,9 @@ describe('ScriptParser', () => {
const expected = 'test-expected-name';
const script = ScriptDataStub.createWithCode()
.withName(expected);
- const compiler = new ScriptCompilerStub();
+ const parseContext = new CategoryCollectionParseContextStub();
// act
- const actual = parseScript(script, compiler);
+ const actual = parseScript(script, parseContext);
// assert
expect(actual.name).to.equal(expected);
});
@@ -26,10 +29,10 @@ describe('ScriptParser', () => {
const docs = [ 'https://expected-doc1.com', 'https://expected-doc2.com' ];
const script = ScriptDataStub.createWithCode()
.withDocs(docs);
- const compiler = new ScriptCompilerStub();
+ const parseContext = new CategoryCollectionParseContextStub();
const expected = parseDocUrls(script);
// act
- const actual = parseScript(script, compiler);
+ const actual = parseScript(script, parseContext);
// assert
expect(actual.documentationUrls).to.deep.equal(expected);
});
@@ -37,44 +40,44 @@ describe('ScriptParser', () => {
it('throws when script is undefined', () => {
// arrange
const expectedError = 'undefined script';
- const compiler = new ScriptCompilerStub();
+ const parseContext = new CategoryCollectionParseContextStub();
const script = undefined;
// act
- const act = () => parseScript(script, compiler);
+ const act = () => parseScript(script, parseContext);
// assert
expect(act).to.throw(expectedError);
});
it('throws when both function call and code are defined', () => {
// arrange
const expectedError = 'cannot define both "call" and "code"';
- const compiler = new ScriptCompilerStub();
+ const parseContext = new CategoryCollectionParseContextStub();
const script = ScriptDataStub
.createWithCall()
.withCode('code');
// act
- const act = () => parseScript(script, compiler);
+ const act = () => parseScript(script, parseContext);
// assert
expect(act).to.throw(expectedError);
});
it('throws when both function call and revertCode are defined', () => {
// arrange
const expectedError = 'cannot define "revertCode" if "call" is defined';
- const compiler = new ScriptCompilerStub();
+ const parseContext = new CategoryCollectionParseContextStub();
const script = ScriptDataStub
.createWithCall()
.withRevertCode('revert-code');
// act
- const act = () => parseScript(script, compiler);
+ const act = () => parseScript(script, parseContext);
// assert
expect(act).to.throw(expectedError);
});
it('throws when neither call or revertCode are defined', () => {
// arrange
const expectedError = 'must define either "call" or "code"';
- const compiler = new ScriptCompilerStub();
+ const parseContext = new CategoryCollectionParseContextStub();
const script = ScriptDataStub.createWithoutCallOrCodes();
// act
- const act = () => parseScript(script, compiler);
+ const act = () => parseScript(script, parseContext);
// assert
expect(act).to.throw(expectedError);
});
@@ -84,11 +87,11 @@ describe('ScriptParser', () => {
const undefinedLevels: string[] = [ '', undefined ];
undefinedLevels.forEach((undefinedLevel) => {
// arrange
- const compiler = new ScriptCompilerStub();
+ const parseContext = new CategoryCollectionParseContextStub();
const script = ScriptDataStub.createWithCode()
.withRecommend(undefinedLevel);
// act
- const actual = parseScript(script, compiler);
+ const actual = parseScript(script, parseContext);
// assert
expect(actual.level).to.equal(undefined);
});
@@ -100,10 +103,10 @@ describe('ScriptParser', () => {
const levelText = 'standard';
const script = ScriptDataStub.createWithCode()
.withRecommend(levelText);
- const compiler = new ScriptCompilerStub();
+ const parseContext = new CategoryCollectionParseContextStub();
const parserMock = mockEnumParser(expectedName, levelText, expectedLevel);
// act
- const actual = parseScript(script, compiler, parserMock);
+ const actual = parseScript(script, parseContext, parserMock);
// assert
expect(actual.level).to.equal(expectedLevel);
});
@@ -115,9 +118,9 @@ describe('ScriptParser', () => {
const script = ScriptDataStub
.createWithCode()
.withCode(expected);
- const compiler = new ScriptCompilerStub();
+ const parseContext = new CategoryCollectionParseContextStub();
// act
- const parsed = parseScript(script, compiler);
+ const parsed = parseScript(script, parseContext);
// assert
const actual = parsed.code.execute;
expect(actual).to.equal(expected);
@@ -128,36 +131,55 @@ describe('ScriptParser', () => {
const script = ScriptDataStub
.createWithCode()
.withRevertCode(expected);
- const compiler = new ScriptCompilerStub();
+ const parseContext = new CategoryCollectionParseContextStub();
// act
- const parsed = parseScript(script, compiler);
+ const parsed = parseScript(script, parseContext);
// assert
const actual = parsed.code.revert;
expect(actual).to.equal(expected);
});
describe('compiler', () => {
- it('throws when compiler is not defined', () => {
+ it('throws when context is not defined', () => {
// arrange
+ const expectedMessage = 'undefined context';
const script = ScriptDataStub.createWithCode();
- const compiler = undefined;
+ const context: ICategoryCollectionParseContext = undefined;
// act
- const act = () => parseScript(script, compiler);
+ const act = () => parseScript(script, context);
// assert
- expect(act).to.throw('undefined compiler');
+ expect(act).to.throw(expectedMessage);
});
it('gets code from compiler', () => {
// arrange
- const expected = new ScriptCode('test-script', 'code', 'revert-code');
+ const expected = new ScriptCodeStub();
const script = ScriptDataStub.createWithCode();
const compiler = new ScriptCompilerStub()
.withCompileAbility(script, expected);
+ const parseContext = new CategoryCollectionParseContextStub()
+ .withCompiler(compiler);
// act
- const parsed = parseScript(script, compiler);
+ const parsed = parseScript(script, parseContext);
// assert
const actual = parsed.code;
expect(actual).to.equal(expected);
});
});
+ describe('syntax', () => {
+ it('set from the context', () => { // test through script validation logic
+ // arrange
+ const commentDelimiter = 'should not throw';
+ const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
+ const parseContext = new CategoryCollectionParseContextStub()
+ .withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
+ const script = ScriptDataStub
+ .createWithoutCallOrCodes()
+ .withCode(duplicatedCode);
+ // act
+ const act = () => parseScript(script, parseContext);
+ // assert
+ expect(act).to.not.throw();
+ });
+ });
});
});
});
diff --git a/tests/unit/application/Parser/Script/Syntax/ConcreteSyntaxes.spec.ts b/tests/unit/application/Parser/Script/Syntax/ConcreteSyntaxes.spec.ts
new file mode 100644
index 00000000..9fad3329
--- /dev/null
+++ b/tests/unit/application/Parser/Script/Syntax/ConcreteSyntaxes.spec.ts
@@ -0,0 +1,33 @@
+import 'mocha';
+import { expect } from 'chai';
+import { ILanguageSyntax } from '@/domain/ScriptCode';
+import { BatchFileSyntax } from '@/application/Parser/Script/Syntax/BatchFileSyntax';
+import { ShellScriptSyntax } from '@/application/Parser/Script/Syntax/ShellScriptSyntax';
+
+
+function getSystemsUnderTest(): ILanguageSyntax[] {
+ return [ new BatchFileSyntax(), new ShellScriptSyntax() ];
+}
+
+describe('ConcreteSyntaxes', () => {
+ describe('commentDelimiters', () => {
+ for (const sut of getSystemsUnderTest()) {
+ it(`${sut.constructor.name} returns defined value`, () => {
+ // act
+ const value = sut.commentDelimiters;
+ // assert
+ expect(value);
+ });
+ }
+ });
+ describe('commonCodeParts', () => {
+ for (const sut of getSystemsUnderTest()) {
+ it(`${sut.constructor.name} returns defined value`, () => {
+ // act
+ const value = sut.commonCodeParts;
+ // assert
+ expect(value);
+ });
+ }
+ });
+});
diff --git a/tests/unit/application/Parser/Script/Syntax/SyntaxFactory.spec.ts b/tests/unit/application/Parser/Script/Syntax/SyntaxFactory.spec.ts
new file mode 100644
index 00000000..7d742b37
--- /dev/null
+++ b/tests/unit/application/Parser/Script/Syntax/SyntaxFactory.spec.ts
@@ -0,0 +1,38 @@
+import 'mocha';
+import { expect } from 'chai';
+import { SyntaxFactory } from '@/application/Parser/Script/Syntax/SyntaxFactory';
+import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
+import { ShellScriptSyntax } from '@/application/Parser/Script/Syntax/ShellScriptSyntax';
+import { BatchFileSyntax } from '@/application/Parser/Script/Syntax/BatchFileSyntax';
+
+describe('SyntaxFactory', () => {
+ describe('getSyntax', () => {
+ describe('creates expected type', () => {
+ it('shellscript returns ShellBuilder', () => {
+ // arrange
+ const testCases: Array< { language: ScriptingLanguage, expected: any} > = [
+ { language: ScriptingLanguage.shellscript, expected: ShellScriptSyntax},
+ { language: ScriptingLanguage.batchfile, expected: BatchFileSyntax},
+ ];
+ for (const testCase of testCases) {
+ it(ScriptingLanguage[testCase.language], () => {
+ // act
+ const sut = new SyntaxFactory();
+ const result = sut.create(testCase.language);
+ // assert
+ expect(result).to.be.instanceOf(testCase.expected,
+ `Actual was: ${result.constructor.name}`);
+ });
+ }
+ });
+ });
+ it('throws on unknown scripting language', () => {
+ // arrange
+ const sut = new SyntaxFactory();
+ // act
+ const act = () => sut.create(3131313131);
+ // assert
+ expect(act).to.throw(`unknown language: "${ScriptingLanguage[3131313131]}"`);
+ });
+ });
+});
diff --git a/tests/unit/application/Parser/ScriptingDefinitionParser.spec.ts b/tests/unit/application/Parser/ScriptingDefinitionParser.spec.ts
index 82d24b73..0a1664e0 100644
--- a/tests/unit/application/Parser/ScriptingDefinitionParser.spec.ts
+++ b/tests/unit/application/Parser/ScriptingDefinitionParser.spec.ts
@@ -34,7 +34,8 @@ describe('ScriptingDefinitionParser', () => {
const expectedName = 'language';
const info = new ProjectInformationStub();
const definition = new ScriptingDefinitionBuilder()
- .withLanguage(languageText).construct();
+ .withLanguage(languageText)
+ .construct();
const parserMock = mockEnumParser(expectedName, languageText, expectedLanguage);
// act
const actual = parseScriptingDefinition(definition, info, new Date(), parserMock);
diff --git a/tests/unit/domain/CategoryCollection.spec.ts b/tests/unit/domain/CategoryCollection.spec.ts
index b71b8f06..eb27a1ea 100644
--- a/tests/unit/domain/CategoryCollection.spec.ts
+++ b/tests/unit/domain/CategoryCollection.spec.ts
@@ -1,6 +1,5 @@
import 'mocha';
import { expect } from 'chai';
-import { ProjectInformation } from '@/domain/ProjectInformation';
import { ICategory } from '@/domain/ICategory';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
diff --git a/tests/unit/domain/Script.spec.ts b/tests/unit/domain/Script.spec.ts
index c24a0d69..02477959 100644
--- a/tests/unit/domain/Script.spec.ts
+++ b/tests/unit/domain/Script.spec.ts
@@ -3,16 +3,15 @@ import 'mocha';
import { expect } from 'chai';
import { Script } from '@/domain/Script';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
-import { ScriptCode } from '@/domain/ScriptCode';
import { IScriptCode } from '@/domain/IScriptCode';
+import { ScriptCodeStub } from '../stubs/ScriptCodeStub';
describe('Script', () => {
describe('ctor', () => {
describe('scriptCode', () => {
it('sets as expected', () => {
// arrange
- const name = 'test-script';
- const expected = new ScriptCode(name, 'expected-execute', 'expected-revert');
+ const expected = new ScriptCodeStub();
const sut = new ScriptBuilder()
.withCode(expected)
.build();
@@ -110,12 +109,14 @@ describe('Script', () => {
class ScriptBuilder {
private name = 'test-script';
- private code: IScriptCode = new ScriptCode(this.name, 'code', 'revert-code');
+ private code: IScriptCode = new ScriptCodeStub();
private level = RecommendationLevel.Standard;
private documentationUrls: readonly string[] = undefined;
public withCodes(code: string, revertCode = ''): ScriptBuilder {
- this.code = new ScriptCode(this.name, code, revertCode);
+ this.code = new ScriptCodeStub()
+ .withExecute(code)
+ .withRevert(revertCode);
return this;
}
diff --git a/tests/unit/domain/ScriptCode.spec.ts b/tests/unit/domain/ScriptCode.spec.ts
index 47a8cbf1..c130f0a5 100644
--- a/tests/unit/domain/ScriptCode.spec.ts
+++ b/tests/unit/domain/ScriptCode.spec.ts
@@ -2,6 +2,8 @@ import 'mocha';
import { expect } from 'chai';
import { ScriptCode } from '@/domain/ScriptCode';
import { IScriptCode } from '@/domain/IScriptCode';
+import { ILanguageSyntax } from '@/domain/ScriptCode';
+import { LanguageSyntaxStub } from '../stubs/LanguageSyntaxStub';
describe('ScriptCode', () => {
describe('scriptName', () => {
@@ -10,7 +12,9 @@ describe('ScriptCode', () => {
const expectedError = 'name is undefined';
const name = undefined;
// act
- const act = () => new ScriptCode(name, 'non-empty-code', '');
+ const act = () => new ScriptCodeBuilder()
+ .withName(name)
+ .build();
// assert
expect(act).to.throw(expectedError);
});
@@ -48,7 +52,11 @@ describe('ScriptCode', () => {
for (const testCase of testCases) {
it(testCase.name, () => {
// act
- const act = () => new ScriptCode(scriptName, testCase.code.execute, testCase.code.revert);
+ const act = () => new ScriptCodeBuilder()
+ .withName(scriptName)
+ .withExecute( testCase.code.execute)
+ .withRevert(testCase.code.revert)
+ .build();
// assert
expect(act).to.throw(testCase.expectedError);
});
@@ -72,15 +80,21 @@ describe('ScriptCode', () => {
// act
const actions = [];
for (const testCase of testCases) {
- const substituteScriptName = (name) => testCase.expectedMessage.replace('$scriptName', name);
+ const substituteScriptName = (name: string) => testCase.expectedMessage.replace('$scriptName', name);
actions.push(...[
{
- act: () => new ScriptCode(scriptName, testCase.code, undefined),
+ act: () => new ScriptCodeBuilder()
+ .withName(scriptName)
+ .withExecute(testCase.code)
+ .build(),
testName: `execute: ${testCase.testName}`,
expectedMessage: substituteScriptName(scriptName),
},
{
- act: () => new ScriptCode(scriptName, 'valid code', testCase.code),
+ act: () => new ScriptCodeBuilder()
+ .withName(scriptName)
+ .withRevert(testCase.code)
+ .build(),
testName: `revert: ${testCase.testName}`,
expectedMessage: substituteScriptName(`${scriptName} (revert)`),
},
@@ -96,26 +110,29 @@ describe('ScriptCode', () => {
});
describe('sets as expected with valid "execute" or "revert"', () => {
// arrange
+ const syntax = new LanguageSyntaxStub()
+ .withCommonCodeParts(')', 'else', '(')
+ .withCommentDelimiters('#', '//');
const testCases = [
{
testName: 'code is a valid string',
code: 'valid code',
},
{
- testName: 'code consists of frequent code parts',
- code: ') else (',
+ testName: 'code consists of common code parts',
+ code: syntax.commonCodeParts.join(' '),
},
{
- testName: 'code is a frequent code part',
- code: ')',
+ testName: 'code is a common code part',
+ code: syntax.commonCodeParts[0],
},
{
- testName: 'code with duplicated comment lines (::)',
- code: ':: comment\n:: comment',
+ testName: `code with duplicated comment lines (${syntax.commentDelimiters[0]})`,
+ code: `${syntax.commentDelimiters[0]} comment\n${syntax.commentDelimiters[0]} comment`,
},
{
- testName: 'code with duplicated comment lines (REM)',
- code: 'REM comment\nREM comment',
+ testName: `code with duplicated comment lines (${syntax.commentDelimiters[1]})`,
+ code: `${syntax.commentDelimiters[1]} comment\n${syntax.commentDelimiters[1]} comment`,
},
];
// act
@@ -124,12 +141,20 @@ describe('ScriptCode', () => {
actions.push(...[
{
testName: `execute: ${testCase.testName}`,
- act: () => createSut(testCase.code),
+ act: () =>
+ new ScriptCodeBuilder()
+ .withSyntax(syntax)
+ .withExecute(testCase.code)
+ .build(),
expect: (sut: IScriptCode) => sut.execute === testCase.code,
},
{
testName: `revert: ${testCase.testName}`,
- act: () => createSut('different code', testCase.code),
+ act: () =>
+ new ScriptCodeBuilder()
+ .withSyntax(syntax)
+ .withRevert(testCase.code)
+ .build(),
expect: (sut: IScriptCode) => sut.revert === testCase.code,
},
]);
@@ -145,6 +170,34 @@ describe('ScriptCode', () => {
});
});
-function createSut(code: string, revert = ''): ScriptCode {
- return new ScriptCode('test-code', code, revert);
+class ScriptCodeBuilder {
+ public execute = 'default-execute-code';
+ public revert = '';
+ public scriptName = 'default-script-name';
+ public syntax: ILanguageSyntax = new LanguageSyntaxStub();
+
+ public withName(name: string) {
+ this.scriptName = name;
+ return this;
+ }
+ public withExecute(execute: string) {
+ this.execute = execute;
+ return this;
+ }
+ public withRevert(revert: string) {
+ this.revert = revert;
+ return this;
+ }
+ public withSyntax(syntax: ILanguageSyntax) {
+ this.syntax = syntax;
+ return this;
+ }
+
+ public build(): ScriptCode {
+ return new ScriptCode(
+ this.execute,
+ this.revert,
+ this.scriptName,
+ this.syntax);
+ }
}
diff --git a/tests/unit/domain/ScriptingDefinition.spec.ts b/tests/unit/domain/ScriptingDefinition.spec.ts
index fcc90a96..2b595b66 100644
--- a/tests/unit/domain/ScriptingDefinition.spec.ts
+++ b/tests/unit/domain/ScriptingDefinition.spec.ts
@@ -3,7 +3,6 @@ import { expect } from 'chai';
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { getEnumValues } from '@/application/Common/Enum';
-import { OperatingSystem } from '@/domain/OperatingSystem';
describe('ScriptingDefinition', () => {
describe('language', () => {
@@ -38,7 +37,7 @@ describe('ScriptingDefinition', () => {
// arrange
const testCases = new Map([
[ScriptingLanguage.batchfile, 'bat'],
- [ScriptingLanguage.bash, 'sh'],
+ [ScriptingLanguage.shellscript, 'sh'],
]);
Array.from(testCases.entries()).forEach((test) => {
const language = test[0];
@@ -108,7 +107,7 @@ describe('ScriptingDefinition', () => {
});
class ScriptingDefinitionBuilder {
- private language = ScriptingLanguage.bash;
+ private language = ScriptingLanguage.shellscript;
private startCode = 'REM start-code';
private endCode = 'REM end-code';
diff --git a/tests/unit/infrastructure/AsyncLazy.spec.ts b/tests/unit/infrastructure/AsyncLazy.spec.ts
index c7c4aa16..2f7345fa 100644
--- a/tests/unit/infrastructure/AsyncLazy.spec.ts
+++ b/tests/unit/infrastructure/AsyncLazy.spec.ts
@@ -35,10 +35,10 @@ describe('AsyncLazy', () => {
expect(results).to.deep.equal([1, 1, 1, 1, 1]);
});
- it('when running long-running task paralelly', async () => {
- const sleep = (time: number) => new Promise(((resolve) => setTimeout(resolve, time)));
+ it('when running long-running task in parallel', async () => {
+ const sleepAsync = (time: number) => new Promise(((resolve) => setTimeout(resolve, time)));
const sut = new AsyncLazy(async () => {
- await sleep(100);
+ await sleepAsync(100);
totalExecuted++;
return Promise.resolve(totalExecuted);
});
diff --git a/tests/unit/infrastructure/Signal.spec.ts b/tests/unit/infrastructure/Signal.spec.ts
index dfd37991..95e39c2c 100644
--- a/tests/unit/infrastructure/Signal.spec.ts
+++ b/tests/unit/infrastructure/Signal.spec.ts
@@ -1,81 +1,87 @@
+import { ISignal } from '@/infrastructure/Events/ISignal';
+import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
import { Signal } from '@/infrastructure/Events/Signal';
import { expect } from 'chai';
+import { EventHandler } from '@/infrastructure/Events/ISignal';
+
describe('Signal', () => {
- class ReceiverMock {
- public onRecieveCalls = new Array();
- public onReceive(arg: number): void { this.onRecieveCalls.push(arg); }
+ class ObserverMock {
+ public readonly onReceiveCalls = new Array();
+ public readonly callbacks = new Array>();
+ public readonly subscription: IEventSubscription;
+ constructor(subject: ISignal) {
+ this.callbacks.push((arg) => this.onReceiveCalls.push(arg));
+ this.subscription = subject.on((arg) => this.callbacks.forEach((action) => action(arg)));
+ }
}
-
let signal: Signal;
beforeEach(() => signal = new Signal());
-
- describe('single reciever', () => {
- let receiver: ReceiverMock;
-
+ describe('single observer', () => {
+ // arrange
+ let observer: ObserverMock;
beforeEach(() => {
- receiver = new ReceiverMock();
- signal.on((arg) => receiver.onReceive(arg));
+ observer = new ObserverMock(signal);
});
-
it('notify() executes the callback', () => {
+ // act
signal.notify(5);
- expect(receiver.onRecieveCalls).to.have.length(1);
+ // assert
+ expect(observer.onReceiveCalls).to.have.length(1);
});
-
it('notify() executes the callback with the payload', () => {
const expected = 5;
+ // act
signal.notify(expected);
- expect(receiver.onRecieveCalls).to.deep.equal([expected]);
+ // assert
+ expect(observer.onReceiveCalls).to.deep.equal([expected]);
+ });
+ it('notify() does not call callback when unsubscribed', () => {
+ // act
+ observer.subscription.unsubscribe();
+ signal.notify(5);
+ // assert
+ expect(observer.onReceiveCalls).to.have.lengthOf(0);
});
});
- describe('multiple recievers', () => {
- let receivers: ReceiverMock[];
-
+ describe('multiple observers', () => {
+ // arrange
+ let observers: ObserverMock[];
beforeEach(() => {
- receivers = [
- new ReceiverMock(), new ReceiverMock(),
- new ReceiverMock(), new ReceiverMock()];
- function subscribeReceiver(receiver: ReceiverMock) {
- signal.on((arg) => receiver.onReceive(arg));
- }
- for (const receiver of receivers) {
- subscribeReceiver(receiver);
- }
+ observers = [
+ new ObserverMock(signal), new ObserverMock(signal),
+ new ObserverMock(signal), new ObserverMock(signal),
+ ];
});
-
-
it('notify() should execute all callbacks', () => {
+ // act
signal.notify(5);
- receivers.forEach((receiver) => {
- expect(receiver.onRecieveCalls).to.have.length(1);
+ // assert
+ observers.forEach((observer) => {
+ expect(observer.onReceiveCalls).to.have.length(1);
});
});
-
it('notify() should execute all callbacks with payload', () => {
const expected = 5;
+ // act
signal.notify(expected);
- receivers.forEach((receiver) => {
- expect(receiver.onRecieveCalls).to.deep.equal([expected]);
+ // assert
+ observers.forEach((observer) => {
+ expect(observer.onReceiveCalls).to.deep.equal([expected]);
});
});
-
it('notify() executes in FIFO order', () => {
// arrange
const expectedSequence = [0, 1, 2, 3];
const actualSequence = new Array();
- for (let i = 0; i < receivers.length; i++) {
- receivers[i].onReceive = ((arg) => {
- actualSequence.push(i);
- });
+ for (let i = 0; i < observers.length; i++) {
+ observers[i].callbacks.push(() => actualSequence.push(i));
}
// act
signal.notify(5);
// assert
expect(actualSequence).to.deep.equal(expectedSequence);
});
-
});
-
});
diff --git a/tests/unit/stubs/CategoryCollectionParseContextStub.ts b/tests/unit/stubs/CategoryCollectionParseContextStub.ts
new file mode 100644
index 00000000..b066eb57
--- /dev/null
+++ b/tests/unit/stubs/CategoryCollectionParseContextStub.ts
@@ -0,0 +1,19 @@
+import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
+import { ScriptCompilerStub } from './ScriptCompilerStub';
+import { LanguageSyntaxStub } from './LanguageSyntaxStub';
+import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
+import { ILanguageSyntax } from '@/domain/ScriptCode';
+
+export class CategoryCollectionParseContextStub implements ICategoryCollectionParseContext {
+ public compiler: IScriptCompiler = new ScriptCompilerStub();
+ public syntax: ILanguageSyntax = new LanguageSyntaxStub();
+
+ public withCompiler(compiler: IScriptCompiler) {
+ this.compiler = compiler;
+ return this;
+ }
+ public withSyntax(syntax: ILanguageSyntax) {
+ this.syntax = syntax;
+ return this;
+ }
+}
diff --git a/tests/unit/stubs/CategoryCollectionStub.ts b/tests/unit/stubs/CategoryCollectionStub.ts
index 920ed3db..15688b0a 100644
--- a/tests/unit/stubs/CategoryCollectionStub.ts
+++ b/tests/unit/stubs/CategoryCollectionStub.ts
@@ -30,6 +30,11 @@ export class CategoryCollectionStub implements ICategoryCollection {
this.initialScript = script;
return this;
}
+ public withTotalScripts(totalScripts: number) {
+ this.totalScripts = totalScripts;
+ return this;
+ }
+
public findCategory(categoryId: number): ICategory {
return this.getAllCategories().find(
(category) => category.id === categoryId);
diff --git a/tests/unit/stubs/FunctionDataStub.ts b/tests/unit/stubs/FunctionDataStub.ts
new file mode 100644
index 00000000..73305108
--- /dev/null
+++ b/tests/unit/stubs/FunctionDataStub.ts
@@ -0,0 +1,25 @@
+import { FunctionData } from 'js-yaml-loader!*';
+
+export class FunctionDataStub implements FunctionData {
+ public name = 'function data stub';
+ public code = 'function data stub code';
+ public revertCode = 'function data stub revertCode';
+ public parameters?: readonly string[];
+
+ public withName(name: string) {
+ this.name = name;
+ return this;
+ }
+ public withParameters(...parameters: string[]) {
+ this.parameters = parameters;
+ return this;
+ }
+ public withCode(code: string) {
+ this.code = code;
+ return this;
+ }
+ public withRevertCode(revertCode: string) {
+ this.revertCode = revertCode;
+ return this;
+ }
+}
diff --git a/tests/unit/stubs/LanguageSyntaxStub.ts b/tests/unit/stubs/LanguageSyntaxStub.ts
new file mode 100644
index 00000000..bb3487b0
--- /dev/null
+++ b/tests/unit/stubs/LanguageSyntaxStub.ts
@@ -0,0 +1,15 @@
+import { ILanguageSyntax } from '@/domain/ScriptCode';
+
+export class LanguageSyntaxStub implements ILanguageSyntax {
+ public commentDelimiters = [];
+ public commonCodeParts = [];
+
+ public withCommentDelimiters(...delimiters: string[]) {
+ this.commentDelimiters = delimiters;
+ return this;
+ }
+ public withCommonCodeParts(...codeParts: string[]) {
+ this.commonCodeParts = codeParts;
+ return this;
+ }
+}
diff --git a/tests/unit/stubs/ScriptCodeStub.ts b/tests/unit/stubs/ScriptCodeStub.ts
new file mode 100644
index 00000000..cac3ab55
--- /dev/null
+++ b/tests/unit/stubs/ScriptCodeStub.ts
@@ -0,0 +1,15 @@
+import { IScriptCode } from '@/domain/IScriptCode';
+
+export class ScriptCodeStub implements IScriptCode {
+ public execute = 'default execute code';
+ public revert = 'default revert code';
+
+ public withExecute(code: string) {
+ this.execute = code;
+ return this;
+ }
+ public withRevert(revert: string) {
+ this.revert = revert;
+ return this;
+ }
+}
diff --git a/tests/unit/stubs/ScriptCompilerStub.ts b/tests/unit/stubs/ScriptCompilerStub.ts
index 9abcbe28..b0015f81 100644
--- a/tests/unit/stubs/ScriptCompilerStub.ts
+++ b/tests/unit/stubs/ScriptCompilerStub.ts
@@ -1,4 +1,4 @@
-import { IScriptCompiler } from '@/application/Parser/Compiler/IScriptCompiler';
+import { IScriptCompiler } from '@/application/Parser/Script/Compiler/IScriptCompiler';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptData } from 'js-yaml-loader!@/*';
diff --git a/tests/unit/stubs/ScriptDataStub.ts b/tests/unit/stubs/ScriptDataStub.ts
index d7d84901..34cf95e7 100644
--- a/tests/unit/stubs/ScriptDataStub.ts
+++ b/tests/unit/stubs/ScriptDataStub.ts
@@ -27,39 +27,30 @@ export class ScriptDataStub implements ScriptData {
public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
public docs = ['hello.com'];
- private constructor() { }
-
public withName(name: string): ScriptDataStub {
this.name = name;
return this;
}
-
public withDocs(docs: string[]): ScriptDataStub {
this.docs = docs;
return this;
}
-
public withCode(code: string): ScriptDataStub {
this.code = code;
return this;
}
-
public withRevertCode(revertCode: string): ScriptDataStub {
this.revertCode = revertCode;
return this;
}
-
public withMockCall(): ScriptDataStub {
this.call = { function: 'func', parameters: [] };
return this;
}
-
public withCall(call: ScriptFunctionCallData): ScriptDataStub {
this.call = call;
return this;
}
-
-
public withRecommend(recommend: string): ScriptDataStub {
this.recommend = recommend;
return this;
diff --git a/tests/unit/stubs/ScriptingDefinitionStub.ts b/tests/unit/stubs/ScriptingDefinitionStub.ts
index 6997481b..629a43af 100644
--- a/tests/unit/stubs/ScriptingDefinitionStub.ts
+++ b/tests/unit/stubs/ScriptingDefinitionStub.ts
@@ -15,4 +15,8 @@ export class ScriptingDefinitionStub implements IScriptingDefinition {
this.endCode = endCode;
return this;
}
+ public withLanguage(language: ScriptingLanguage): ScriptingDefinitionStub {
+ this.language = language;
+ return this;
+ }
}