add initial macOS support #40
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# privacy.sexy
|
# 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)
|
[](./CONTRIBUTING.md)
|
||||||
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.8.2",
|
"version": "0.8.2",
|
||||||
"author": "undergroundwires",
|
"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",
|
"homepage": "https://privacy.sexy",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows</title>
|
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows and macOS</title>
|
||||||
<meta name="robots" content="index,follow" />
|
<meta name="robots" content="index,follow" />
|
||||||
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { Component, Vue } from 'vue-property-decorator';
|
|||||||
import TheHeader from '@/presentation/TheHeader.vue';
|
import TheHeader from '@/presentation/TheHeader.vue';
|
||||||
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
||||||
import TheCodeArea from '@/presentation/TheCodeArea.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 TheSearchBar from '@/presentation/TheSearchBar.vue';
|
||||||
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
|
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,8 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
||||||
}
|
}
|
||||||
const event: IApplicationContextChangedEvent = {
|
const event: IApplicationContextChangedEvent = {
|
||||||
newState: this.state,
|
newState: this.states[os],
|
||||||
newCollection: this.collection,
|
oldState: this.states[this.currentOs],
|
||||||
newOs: os,
|
|
||||||
};
|
};
|
||||||
this.contextChanged.notify(event);
|
this.contextChanged.notify(event);
|
||||||
this.currentOs = os;
|
this.currentOs = os;
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSy
|
|||||||
return currentOs;
|
return currentOs;
|
||||||
}
|
}
|
||||||
supportedOsList.sort((os1, os2) => {
|
supportedOsList.sort((os1, os2) => {
|
||||||
const os1SupportLevel = app.collections[os1].totalScripts;
|
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||||
const os2SupportLevel = app.collections[os2].totalScripts;
|
return getPriority(os2) - getPriority(os1);
|
||||||
return os1SupportLevel - os2SupportLevel;
|
|
||||||
});
|
});
|
||||||
return supportedOsList[0];
|
return supportedOsList[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
export interface IApplicationContext {
|
export interface IApplicationContext {
|
||||||
readonly currentOs: OperatingSystem;
|
|
||||||
readonly app: IApplication;
|
readonly app: IApplication;
|
||||||
readonly collection: ICategoryCollection;
|
|
||||||
readonly state: ICategoryCollectionState;
|
readonly state: ICategoryCollectionState;
|
||||||
readonly contextChanged: ISignal<IApplicationContextChangedEvent>;
|
readonly contextChanged: ISignal<IApplicationContextChangedEvent>;
|
||||||
changeContext(os: OperatingSystem): void;
|
changeContext(os: OperatingSystem): void;
|
||||||
@@ -15,6 +12,5 @@ export interface IApplicationContext {
|
|||||||
|
|
||||||
export interface IApplicationContextChangedEvent {
|
export interface IApplicationContextChangedEvent {
|
||||||
readonly newState: ICategoryCollectionState;
|
readonly newState: ICategoryCollectionState;
|
||||||
readonly newCollection: ICategoryCollection;
|
readonly oldState: ICategoryCollectionState;
|
||||||
readonly newOs: OperatingSystem;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import { IUserSelection } from './Selection/IUserSelection';
|
|||||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
|
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||||
|
public readonly os: OperatingSystem;
|
||||||
public readonly code: IApplicationCode;
|
public readonly code: IApplicationCode;
|
||||||
public readonly selection: IUserSelection;
|
public readonly selection: IUserSelection;
|
||||||
public readonly filter: IUserFilter;
|
public readonly filter: IUserFilter;
|
||||||
@@ -16,5 +18,6 @@ export class CategoryCollectionState implements ICategoryCollectionState {
|
|||||||
this.selection = new UserSelection(collection, []);
|
this.selection = new UserSelection(collection, []);
|
||||||
this.code = new ApplicationCode(this.selection, collection.scripting);
|
this.code = new ApplicationCode(this.selection, collection.scripting);
|
||||||
this.filter = new UserFilter(collection);
|
this.filter = new UserFilter(collection);
|
||||||
|
this.os = collection.os;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ICodeBuilder } from './ICodeBuilder';
|
|||||||
const NewLine = '\n';
|
const NewLine = '\n';
|
||||||
const TotalFunctionSeparatorChars = 58;
|
const TotalFunctionSeparatorChars = 58;
|
||||||
|
|
||||||
export class CodeBuilder implements ICodeBuilder {
|
export abstract class CodeBuilder implements ICodeBuilder {
|
||||||
private readonly lines = new Array<string>();
|
private readonly lines = new Array<string>();
|
||||||
|
|
||||||
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
// 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 {
|
public appendCommentLine(commentLine?: string): CodeBuilder {
|
||||||
this.lines.push(`:: ${commentLine}`);
|
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,9 +37,8 @@ export class CodeBuilder implements ICodeBuilder {
|
|||||||
if (!name) { throw new Error('name cannot be empty or null'); }
|
if (!name) { throw new Error('name cannot be empty or null'); }
|
||||||
if (!code) { throw new Error('code cannot be empty or null'); }
|
if (!code) { throw new Error('code cannot be empty or null'); }
|
||||||
return this
|
return this
|
||||||
.appendLine()
|
|
||||||
.appendCommentLineWithHyphensAround(name)
|
.appendCommentLineWithHyphensAround(name)
|
||||||
.appendLine(`echo --- ${name}`)
|
.appendLine(this.writeStandardOut(`--- ${name}`))
|
||||||
.appendLine(code)
|
.appendLine(code)
|
||||||
.appendTrailingHyphensCommentLine();
|
.appendTrailingHyphensCommentLine();
|
||||||
}
|
}
|
||||||
@@ -62,4 +61,7 @@ export class CodeBuilder implements ICodeBuilder {
|
|||||||
public toString(): string {
|
public toString(): string {
|
||||||
return this.lines.join(NewLine);
|
return this.lines.join(NewLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract getCommentDelimiter(): string;
|
||||||
|
protected abstract writeStandardOut(text: string): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
|
export interface ICodeBuilderFactory {
|
||||||
|
create(language: ScriptingLanguage): ICodeBuilder;
|
||||||
|
}
|
||||||
@@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}'`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
import { CodeBuilder } from './CodeBuilder';
|
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { CodePosition } from '../Position/CodePosition';
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
import { IUserScript } from './IUserScript';
|
import { IUserScript } from './IUserScript';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
|
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
||||||
|
|
||||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||||
constructor(private readonly codeBuilderFactory: () => ICodeBuilder = () => new CodeBuilder()) {
|
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
||||||
|
|
||||||
}
|
}
|
||||||
public buildCode(
|
public buildCode(
|
||||||
@@ -20,7 +21,7 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
|||||||
if (!selectedScripts.length) {
|
if (!selectedScripts.length) {
|
||||||
return { code: '', scriptPositions };
|
return { code: '', scriptPositions };
|
||||||
}
|
}
|
||||||
let builder = this.codeBuilderFactory();
|
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
||||||
builder = initializeCode(scriptingDefinition.startCode, builder);
|
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||||
for (const selection of selectedScripts) {
|
for (const selection of selectedScripts) {
|
||||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||||
@@ -52,16 +53,19 @@ function appendSelection(
|
|||||||
selection: SelectedScript,
|
selection: SelectedScript,
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||||
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
|
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||||
const startPosition = builder.currentLine + 1;
|
const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
|
||||||
appendCode(selection, builder);
|
builder = appendCode(selection, builder);
|
||||||
const endPosition = builder.currentLine - 1;
|
const endPosition = builder.currentLine - 1;
|
||||||
builder.appendLine();
|
builder.appendLine();
|
||||||
scriptPositions.set(selection, new CodePosition(startPosition, endPosition));
|
const position = new CodePosition(startPosition, endPosition);
|
||||||
|
scriptPositions.set(selection, position);
|
||||||
return scriptPositions;
|
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 name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||||
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
||||||
builder.appendFunction(name, scriptCode);
|
return builder
|
||||||
|
.appendLine()
|
||||||
|
.appendFunction(name, scriptCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ICodePosition } from './ICodePosition';
|
import { ICodePosition } from './ICodePosition';
|
||||||
export class CodePosition implements ICodePosition {
|
|
||||||
|
|
||||||
|
export class CodePosition implements ICodePosition {
|
||||||
public get totalLines(): number {
|
public get totalLines(): number {
|
||||||
return this.endLine - this.startLine;
|
return this.endLine - this.startLine;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
|
||||||
|
|
||||||
export interface IUserFilter {
|
export interface IUserFilter {
|
||||||
readonly currentFilter: IFilterResult | undefined;
|
readonly currentFilter: IFilterResult | undefined;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { IUserFilter } from './Filter/IUserFilter';
|
import { IUserFilter } from './Filter/IUserFilter';
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
import { IUserSelection } from './Selection/IUserSelection';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
export { IUserSelection, IApplicationCode, IUserFilter };
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface ICategoryCollectionState {
|
export interface ICategoryCollectionState {
|
||||||
readonly code: IApplicationCode;
|
readonly code: IApplicationCode;
|
||||||
readonly filter: IUserFilter;
|
readonly filter: IUserFilter;
|
||||||
readonly selection: IUserSelection;
|
readonly selection: IUserSelection;
|
||||||
|
readonly collection: ICategoryCollection;
|
||||||
|
readonly os: OperatingSystem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
import { SelectedScript } from './SelectedScript';
|
||||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
|
|
||||||
export interface IUserSelection {
|
export interface IUserSelection {
|
||||||
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
|||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
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 { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
@@ -10,10 +11,11 @@ import { Application } from '@/domain/Application';
|
|||||||
export function parseApplication(
|
export function parseApplication(
|
||||||
parser = CategoryCollectionParser,
|
parser = CategoryCollectionParser,
|
||||||
processEnv: NodeJS.ProcessEnv = process.env,
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
collectionData = LoadedCollectionData): IApplication {
|
collectionsData = PreParsedCollections): IApplication {
|
||||||
|
validateCollectionsData(collectionsData);
|
||||||
const information = parseProjectInformation(processEnv);
|
const information = parseProjectInformation(processEnv);
|
||||||
const collection = parser(collectionData, information);
|
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||||
const app = new Application(information, [ collection ]);
|
const app = new Application(information, collections);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,5 +25,14 @@ export type CategoryCollectionParserType
|
|||||||
const CategoryCollectionParser: CategoryCollectionParserType
|
const CategoryCollectionParser: CategoryCollectionParserType
|
||||||
= (file, info) => parseCategoryCollection(file, info);
|
= (file, info) => parseCategoryCollection(file, info);
|
||||||
|
|
||||||
const LoadedCollectionData: CollectionData
|
const PreParsedCollections: readonly CollectionData []
|
||||||
= WindowsData;
|
= [ 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
import { parseCategory } from './CategoryParser';
|
import { parseCategory } from './CategoryParser';
|
||||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { parseScriptingDefinition } from './ScriptingDefinitionParser';
|
import { parseScriptingDefinition } from './ScriptingDefinitionParser';
|
||||||
import { createEnumParser } from '../Common/Enum';
|
import { createEnumParser } from '../Common/Enum';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||||
|
|
||||||
export function parseCategoryCollection(
|
export function parseCategoryCollection(
|
||||||
content: CollectionData,
|
content: CollectionData,
|
||||||
info: IProjectInformation,
|
info: IProjectInformation,
|
||||||
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
||||||
validate(content);
|
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<Category>();
|
const categories = new Array<Category>();
|
||||||
for (const action of content.actions) {
|
for (const action of content.actions) {
|
||||||
const category = parseCategory(action, compiler);
|
const category = parseCategory(action, context);
|
||||||
categories.push(category);
|
categories.push(category);
|
||||||
}
|
}
|
||||||
const os = osParser.parseEnum(content.os, 'os');
|
const os = osParser.parseEnum(content.os, 'os');
|
||||||
const scripting = parseScriptingDefinition(content.scripting, info);
|
|
||||||
const collection = new CategoryCollection(
|
const collection = new CategoryCollection(
|
||||||
os,
|
os,
|
||||||
categories,
|
categories,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@
|
|||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
import { parseScript } from './ScriptParser';
|
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
import { parseScript } from './Script/ScriptParser';
|
||||||
|
|
||||||
let categoryIdCounter: number = 0;
|
let categoryIdCounter: number = 0;
|
||||||
|
|
||||||
@@ -12,17 +12,15 @@ interface ICategoryChildren {
|
|||||||
subScripts: Script[];
|
subScripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCategory(category: CategoryData, compiler: IScriptCompiler): Category {
|
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
|
||||||
if (!compiler) {
|
if (!context) { throw new Error('undefined context'); }
|
||||||
throw new Error('undefined compiler');
|
|
||||||
}
|
|
||||||
ensureValid(category);
|
ensureValid(category);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
subScripts: new Array<Script>(),
|
subScripts: new Array<Script>(),
|
||||||
};
|
};
|
||||||
for (const data of category.children) {
|
for (const data of category.children) {
|
||||||
parseCategoryChild(data, children, category, compiler);
|
parseCategoryChild(data, children, category, context);
|
||||||
}
|
}
|
||||||
return new Category(
|
return new Category(
|
||||||
/*id*/ categoryIdCounter++,
|
/*id*/ categoryIdCounter++,
|
||||||
@@ -49,13 +47,13 @@ function parseCategoryChild(
|
|||||||
data: CategoryOrScriptData,
|
data: CategoryOrScriptData,
|
||||||
children: ICategoryChildren,
|
children: ICategoryChildren,
|
||||||
parent: CategoryData,
|
parent: CategoryData,
|
||||||
compiler: IScriptCompiler) {
|
context: ICategoryCollectionParseContext) {
|
||||||
if (isCategory(data)) {
|
if (isCategory(data)) {
|
||||||
const subCategory = parseCategory(data as CategoryData, compiler);
|
const subCategory = parseCategory(data as CategoryData, context);
|
||||||
children.subCategories.push(subCategory);
|
children.subCategories.push(subCategory);
|
||||||
} else if (isScript(data)) {
|
} else if (isScript(data)) {
|
||||||
const scriptData = data as ScriptData;
|
const scriptData = data as ScriptData;
|
||||||
const script = parseScript(scriptData, compiler);
|
const script = parseScript(scriptData, context);
|
||||||
children.subScripts.push(script);
|
children.subScripts.push(script);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Child element is neither a category or a script.
|
throw new Error(`Child element is neither a category or a script.
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { FunctionData } from 'js-yaml-loader!*';
|
||||||
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||||
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
||||||
|
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
||||||
|
|
||||||
|
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||||
|
public readonly compiler: IScriptCompiler;
|
||||||
|
public readonly syntax: ILanguageSyntax;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
|
scripting: IScriptingDefinition,
|
||||||
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) {
|
||||||
|
if (!scripting) { throw new Error('undefined scripting'); }
|
||||||
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
|
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { IScriptCode } from '@/domain/IScriptCode';
|
|||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { ScriptData, FunctionData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
import { ScriptData, FunctionData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
||||||
import { IScriptCompiler } from './IScriptCompiler';
|
import { IScriptCompiler } from './IScriptCompiler';
|
||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
interface ICompiledCode {
|
interface ICompiledCode {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
@@ -10,8 +11,11 @@ interface ICompiledCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ScriptCompiler implements IScriptCompiler {
|
export class ScriptCompiler implements IScriptCompiler {
|
||||||
constructor(private readonly functions: readonly FunctionData[]) {
|
constructor(
|
||||||
|
private readonly functions: readonly FunctionData[] | undefined,
|
||||||
|
private syntax: ILanguageSyntax) {
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
|
if (!syntax) { throw new Error('undefined syntax'); }
|
||||||
}
|
}
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
if (!script.call) {
|
if (!script.call) {
|
||||||
@@ -33,7 +37,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
compiledCodes.push(functionCode);
|
compiledCodes.push(functionCode);
|
||||||
});
|
});
|
||||||
const scriptCode = merge(compiledCodes);
|
const scriptCode = merge(compiledCodes);
|
||||||
return new ScriptCode(script.name, scriptCode.code, scriptCode.revertCode);
|
return new ScriptCode(scriptCode.code, scriptCode.revertCode, script.name, this.syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFunctionByName(name: string): FunctionData {
|
private getFunctionByName(name: string): FunctionData {
|
||||||
@@ -43,7 +47,6 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
}
|
}
|
||||||
return func;
|
return func;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureCompilable(call: ScriptFunctionCallData) {
|
private ensureCompilable(call: ScriptFunctionCallData) {
|
||||||
if (!this.functions || this.functions.length === 0) {
|
if (!this.functions || this.functions.length === 0) {
|
||||||
throw new Error('cannot compile without shared functions');
|
throw new Error('cannot compile without shared functions');
|
||||||
@@ -69,7 +72,11 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
|||||||
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||||
|
if (functions.some((func) => !func)) {
|
||||||
|
throw new Error(`some functions are undefined`);
|
||||||
|
}
|
||||||
|
}
|
||||||
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
||||||
const functionsWithParameters = functions
|
const functionsWithParameters = functions
|
||||||
.filter((func) => func.parameters && func.parameters.length > 0);
|
.filter((func) => func.parameters && func.parameters.length > 0);
|
||||||
@@ -95,9 +102,10 @@ function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||||
if (!functions) {
|
if (!functions || functions.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ensureNoUndefinedItem(functions);
|
||||||
ensureNoDuplicatesInFunctionNames(functions);
|
ensureNoDuplicatesInFunctionNames(functions);
|
||||||
ensureNoDuplicatesInParameterNames(functions);
|
ensureNoDuplicatesInParameterNames(functions);
|
||||||
ensureNoDuplicateCode(functions);
|
ensureNoDuplicateCode(functions);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
|
||||||
|
export interface ICategoryCollectionParseContext {
|
||||||
|
readonly compiler: IScriptCompiler;
|
||||||
|
readonly syntax: ILanguageSyntax;
|
||||||
|
}
|
||||||
@@ -1,22 +1,20 @@
|
|||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from '../DocumentationParser';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { createEnumParser, IEnumParser } from '../Common/Enum';
|
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
||||||
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
|
||||||
export function parseScript(
|
export function parseScript(
|
||||||
data: ScriptData, compiler: IScriptCompiler,
|
data: ScriptData, context: ICategoryCollectionParseContext,
|
||||||
levelParser = createEnumParser(RecommendationLevel)): Script {
|
levelParser = createEnumParser(RecommendationLevel)): Script {
|
||||||
validateScript(data);
|
validateScript(data);
|
||||||
if (!compiler) {
|
if (!context) { throw new Error('undefined context'); }
|
||||||
throw new Error('undefined compiler');
|
|
||||||
}
|
|
||||||
const script = new Script(
|
const script = new Script(
|
||||||
/* name */ data.name,
|
/* name */ data.name,
|
||||||
/* code */ parseCode(data, compiler),
|
/* code */ parseCode(data, context),
|
||||||
/* docs */ parseDocUrls(data),
|
/* docs */ parseDocUrls(data),
|
||||||
/* level */ parseLevel(data.recommend, levelParser));
|
/* level */ parseLevel(data.recommend, levelParser));
|
||||||
return script;
|
return script;
|
||||||
@@ -29,11 +27,11 @@ function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): Re
|
|||||||
return parser.parseEnum(level, 'level');
|
return parser.parseEnum(level, 'level');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCode(script: ScriptData, compiler: IScriptCompiler): IScriptCode {
|
function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode {
|
||||||
if (compiler.canCompile(script)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return compiler.compile(script);
|
return context.compiler.compile(script);
|
||||||
}
|
}
|
||||||
return new ScriptCode(script.name, script.code, script.revertCode);
|
return new ScriptCode(script.code, script.revertCode, script.name, context.syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNotBothCallAndCode(script: ScriptData) {
|
function ensureNotBothCallAndCode(script: ScriptData) {
|
||||||
6
src/application/Parser/Script/Syntax/BatchFileSyntax.ts
Normal file
6
src/application/Parser/Script/Syntax/BatchFileSyntax.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
|
export class BatchFileSyntax implements ILanguageSyntax {
|
||||||
|
public readonly commentDelimiters = [ 'REM', '::' ];
|
||||||
|
public readonly commonCodeParts = [ '(', ')', 'else' ];
|
||||||
|
}
|
||||||
6
src/application/Parser/Script/Syntax/ISyntaxFactory.ts
Normal file
6
src/application/Parser/Script/Syntax/ISyntaxFactory.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
|
||||||
|
export interface ISyntaxFactory {
|
||||||
|
create(language: ScriptingLanguage): ILanguageSyntax;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
|
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||||
|
public readonly commentDelimiters = [ '#' ];
|
||||||
|
public readonly commonCodeParts = [ '(', ')', 'else' ];
|
||||||
|
}
|
||||||
15
src/application/Parser/Script/Syntax/SyntaxFactory.ts
Normal file
15
src/application/Parser/Script/Syntax/SyntaxFactory.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||||
|
import { BatchFileSyntax } from './BatchFileSyntax';
|
||||||
|
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||||
|
|
||||||
|
export class SyntaxFactory implements ISyntaxFactory {
|
||||||
|
public create(language: ScriptingLanguage): ILanguageSyntax {
|
||||||
|
switch (language) {
|
||||||
|
case ScriptingLanguage.batchfile: return new BatchFileSyntax();
|
||||||
|
case ScriptingLanguage.shellscript: return new ShellScriptSyntax();
|
||||||
|
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { createEnumParser } from '../Common/Enum';
|
import { createEnumParser } from '../Common/Enum';
|
||||||
import { generateIlCode } from './Compiler/ILCode';
|
import { generateIlCode } from './Script/Compiler/ILCode';
|
||||||
|
|
||||||
export function parseScriptingDefinition(
|
export function parseScriptingDefinition(
|
||||||
definition: ScriptingDefinitionData,
|
definition: ScriptingDefinitionData,
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ declare module 'js-yaml-loader!*' {
|
|||||||
|
|
||||||
export interface ScriptData extends DocumentableData {
|
export interface ScriptData extends DocumentableData {
|
||||||
name: string;
|
name: string;
|
||||||
code: string | undefined;
|
code?: string;
|
||||||
revertCode: string | undefined;
|
revertCode?: string;
|
||||||
call: ScriptFunctionCallData;
|
call: ScriptFunctionCallData;
|
||||||
recommend: string | undefined;
|
recommend?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptingDefinitionData {
|
export interface ScriptingDefinitionData {
|
||||||
|
|||||||
225
src/application/collections/macos.yaml
Normal file
225
src/application/collections/macos.yaml
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# Structure documented in "docs/collections.md"
|
||||||
|
os: macos
|
||||||
|
scripting:
|
||||||
|
language: shellscript
|
||||||
|
startCode: |-
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# {{ $homepage }} — v{{ $version }} — {{ $date }}
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
script_path=$([[ "$0" = /* ]] && echo "$0" || echo "$PWD/${0#./}")
|
||||||
|
sudo "$script_path" || (
|
||||||
|
echo 'Administrator privileges are required.'
|
||||||
|
exit 1
|
||||||
|
)
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
endCode: |-
|
||||||
|
echo 'Your privacy and security is now hardened 🎉💪'
|
||||||
|
echo 'Press any key to exit.'
|
||||||
|
read -n 1 -s
|
||||||
|
actions:
|
||||||
|
-
|
||||||
|
category: Privacy cleanup
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
category: Clear terminal history
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Clear bash history
|
||||||
|
recommend: standard
|
||||||
|
code: rm -f ~/.bash_history
|
||||||
|
-
|
||||||
|
name: Clear zsh history
|
||||||
|
recommend: standard
|
||||||
|
code: rm -f ~/.zsh_history
|
||||||
|
-
|
||||||
|
name: Clear CUPS printer job cache
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
sudo rm -rfv /var/spool/cups/c0*
|
||||||
|
sudo rm -rfv /var/spool/cups/tmp/*
|
||||||
|
sudo rm -rfv /var/spool/cups/cache/job.cache*
|
||||||
|
-
|
||||||
|
name: Clear the list of iOS devices connected
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
sudo defaults delete /Users/$USER/Library/Preferences/com.apple.iPod.plist "conn:128:Last Connect"
|
||||||
|
sudo defaults delete /Users/$USER/Library/Preferences/com.apple.iPod.plist Devices
|
||||||
|
sudo defaults delete /Library/Preferences/com.apple.iPod.plist "conn:128:Last Connect"
|
||||||
|
sudo defaults delete /Library/Preferences/com.apple.iPod.plist Devices
|
||||||
|
sudo rm -rfv /var/db/lockdown/*
|
||||||
|
-
|
||||||
|
name: Reset privacy database (remove all permissions)
|
||||||
|
code: sudo tccutil reset All
|
||||||
|
-
|
||||||
|
category: Configure programs
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable Firefox telemetry
|
||||||
|
recommend: standard
|
||||||
|
docs: https://github.com/mozilla/policy-templates/blob/master/README.md
|
||||||
|
code: |-
|
||||||
|
# Enable Firefox policies so the telemetry can be configured.
|
||||||
|
sudo defaults write /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled -bool TRUE
|
||||||
|
# Disable sending usage data
|
||||||
|
sudo defaults write /Library/Preferences/org.mozilla.firefox DisableTelemetry -bool TRUE
|
||||||
|
revertCode: |-
|
||||||
|
sudo defaults delete /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled
|
||||||
|
sudo defaults delete /Library/Preferences/org.mozilla.firefox DisableTelemetry
|
||||||
|
-
|
||||||
|
name: Disable Microsoft Office diagnostics data sending
|
||||||
|
recommend: standard
|
||||||
|
code: defaults write com.microsoft.office DiagnosticDataTypePreference -string ZeroDiagnosticData
|
||||||
|
revertCode: defaults delete com.microsoft.office DiagnosticDataTypePreference
|
||||||
|
-
|
||||||
|
name: Uninstall Google update
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
googleUpdateFile=~/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/Resources/ksinstall
|
||||||
|
if [ -f "$googleUpdateFile" ]; then
|
||||||
|
$googleUpdateFile --nuke
|
||||||
|
echo Uninstalled google update
|
||||||
|
else
|
||||||
|
echo Google update file does not exist
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Disable Homebrew user behavior analytics
|
||||||
|
recommend: standard
|
||||||
|
docs: https://docs.brew.sh/Analytics
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: PersistUserEnvironmentConfiguration
|
||||||
|
parameters:
|
||||||
|
configuration: export HOMEBREW_NO_ANALYTICS=1
|
||||||
|
-
|
||||||
|
name: Disable NET Core CLI telemetry
|
||||||
|
recommend: standard
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: PersistUserEnvironmentConfiguration
|
||||||
|
parameters:
|
||||||
|
configuration: export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
-
|
||||||
|
name: Disable PowerShell Core telemetry
|
||||||
|
recommend: standard
|
||||||
|
docs: https://github.com/PowerShell/PowerShell/tree/release/v7.1.1#telemetry
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: PersistUserEnvironmentConfiguration
|
||||||
|
parameters:
|
||||||
|
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
|
||||||
|
-
|
||||||
|
category: Configure OS
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
category: Configure Apple Remote Desktop
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Deactivate the Remote Management Service
|
||||||
|
recommend: strict
|
||||||
|
code: sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -deactivate -stop
|
||||||
|
revertCode: sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -activate -restart -agent -console
|
||||||
|
-
|
||||||
|
name: Remove Apple Remote Desktop Settings
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
sudo rm -rf /var/db/RemoteManagement
|
||||||
|
sudo defaults delete /Library/Preferences/com.apple.RemoteDesktop.plist
|
||||||
|
defaults delete ~/Library/Preferences/com.apple.RemoteDesktop.plist
|
||||||
|
sudo rm -r /Library/Application\ Support/Apple/Remote\ Desktop/
|
||||||
|
rm -r ~/Library/Application\ Support/Remote\ Desktop/
|
||||||
|
rm -r ~/Library/Containers/com.apple.RemoteDesktop
|
||||||
|
-
|
||||||
|
name: Disable Internet based spell correction
|
||||||
|
code: defaults write NSGlobalDomain WebAutomaticSpellingCorrectionEnabled -bool false
|
||||||
|
revertCode: defaults delete NSGlobalDomain WebAutomaticSpellingCorrectionEnabled
|
||||||
|
-
|
||||||
|
name: Disable Remote Apple Events
|
||||||
|
recommend: strict
|
||||||
|
code: sudo systemsetup -setremoteappleevents off
|
||||||
|
revertCode: sudo systemsetup -setremoteappleevents on
|
||||||
|
-
|
||||||
|
name: Do not store documents to iCloud Drive by default
|
||||||
|
docs: https://macos-defaults.com/finder/nsdocumentsavenewdocumentstocloud.html
|
||||||
|
recommend: standard
|
||||||
|
code: defaults write NSGlobalDomain NSDocumentSaveNewDocumentsToCloud -bool false
|
||||||
|
revertCode: defaults delete NSGlobalDomain NSDocumentSaveNewDocumentsToCloud
|
||||||
|
-
|
||||||
|
category: Security improvements
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
category: Configure macOS Application Firewall
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Enable firewall
|
||||||
|
recommend: standard
|
||||||
|
docs: https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81681
|
||||||
|
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
|
||||||
|
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate off
|
||||||
|
-
|
||||||
|
name: Turn on firewall logging
|
||||||
|
recommend: standard
|
||||||
|
docs: https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81671
|
||||||
|
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode on
|
||||||
|
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode off
|
||||||
|
-
|
||||||
|
name: Turn on stealth mode
|
||||||
|
recommend: standard
|
||||||
|
docs: https://www.stigviewer.com/stig/apple_os_x_10.8_mountain_lion_workstation/2015-02-10/finding/V-51327
|
||||||
|
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on
|
||||||
|
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode off
|
||||||
|
-
|
||||||
|
name: Disable Spotlight indexing
|
||||||
|
code: sudo mdutil -i off -d /
|
||||||
|
revertCode: sudo mdutil -i on /
|
||||||
|
-
|
||||||
|
name: Disable Captive portal
|
||||||
|
docs:
|
||||||
|
- https://web.archive.org/web/20171008071031if_/http://blog.erratasec.com/2010/09/apples-secret-wispr-request.html#.WdnPa5OyL6Y
|
||||||
|
- https://web.archive.org/web/20130407200745/http://www.divertednetworks.net/apple-captiveportal.html
|
||||||
|
- https://web.archive.org/web/20170622064304/https://grpugh.wordpress.com/2014/10/29/an-undocumented-change-to-captive-network-assistant-settings-in-os-x-10-10-yosemite/
|
||||||
|
code: sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.captive.control.plist Active -bool false
|
||||||
|
revertCode: sudo defaults delete /Library/Preferences/SystemConfiguration/com.apple.captive.control.plist Active
|
||||||
|
-
|
||||||
|
name: Require a password to wake the computer from sleep or screen saver
|
||||||
|
code: defaults write /Library/Preferences/com.apple.screensaver askForPassword -bool true
|
||||||
|
revertCode: sudo defaults delete /Library/Preferences/com.apple.screensaver askForPassword
|
||||||
|
-
|
||||||
|
name: Do not show recent items on dock
|
||||||
|
docs: https://developer.apple.com/documentation/devicemanagement/dock
|
||||||
|
code: defaults write com.apple.dock show-recents -bool false
|
||||||
|
revertCode: defaults delete com.apple.dock show-recents
|
||||||
|
-
|
||||||
|
name: Disable AirDrop file sharing
|
||||||
|
recommend: strict
|
||||||
|
code: defaults write com.apple.NetworkBrowser DisableAirDrop -bool true
|
||||||
|
revertCode: defaults write com.apple.NetworkBrowser DisableAirDrop -bool false
|
||||||
|
functions:
|
||||||
|
-
|
||||||
|
name: PersistUserEnvironmentConfiguration
|
||||||
|
parameters: [ configuration ]
|
||||||
|
code: |-
|
||||||
|
command='{{ $configuration }}'
|
||||||
|
declare -a profile_files=("$HOME/.bash_profile" "$HOME/.zprofile")
|
||||||
|
for profile_file in "${profile_files[@]}"
|
||||||
|
do
|
||||||
|
touch "$profile_file"
|
||||||
|
if ! grep -q "$command" "${profile_file}"; then
|
||||||
|
echo "$command" >> "$profile_file"
|
||||||
|
echo "[$profile_file] Configured"
|
||||||
|
else
|
||||||
|
echo "[$profile_file] No need for any action, already configured"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
revertCode: |-
|
||||||
|
command='{{ $configuration }}'
|
||||||
|
declare -a profile_files=("$HOME/.bash_profile" "$HOME/.zprofile")
|
||||||
|
for profile_file in "${profile_files[@]}"
|
||||||
|
do
|
||||||
|
if grep -q "$command" "${profile_file}" 2>/dev/null; then
|
||||||
|
sed -i '' "/$command/d" "$profile_file"
|
||||||
|
echo "[$profile_file] Reverted configuration"
|
||||||
|
else
|
||||||
|
echo "[$profile_file] No need for any action, configuration does not exist"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -2,16 +2,16 @@ import { IScriptCode } from './IScriptCode';
|
|||||||
|
|
||||||
export class ScriptCode implements IScriptCode {
|
export class ScriptCode implements IScriptCode {
|
||||||
constructor(
|
constructor(
|
||||||
scriptName: string,
|
|
||||||
public readonly execute: string,
|
public readonly execute: string,
|
||||||
public readonly revert: string) {
|
public readonly revert: string,
|
||||||
if (!scriptName) {
|
scriptName: string,
|
||||||
throw new Error('script name is undefined');
|
syntax: ILanguageSyntax) {
|
||||||
}
|
if (!scriptName) { throw new Error('script name is undefined'); }
|
||||||
validateCode(scriptName, execute);
|
if (!syntax) { throw new Error('syntax is undefined'); }
|
||||||
|
validateCode(scriptName, execute, syntax);
|
||||||
if (revert) {
|
if (revert) {
|
||||||
scriptName = `${scriptName} (revert)`;
|
scriptName = `${scriptName} (revert)`;
|
||||||
validateCode(scriptName, revert);
|
validateCode(scriptName, revert, syntax);
|
||||||
if (execute === revert) {
|
if (execute === revert) {
|
||||||
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
|
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
|
||||||
}
|
}
|
||||||
@@ -19,12 +19,17 @@ export class ScriptCode implements IScriptCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(name: string, code: string): void {
|
export interface ILanguageSyntax {
|
||||||
|
readonly commentDelimiters: string[];
|
||||||
|
readonly commonCodeParts: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCode(name: string, code: string, syntax: ILanguageSyntax): void {
|
||||||
if (!code || code.length === 0) {
|
if (!code || code.length === 0) {
|
||||||
throw new Error(`code of ${name} is empty or undefined`);
|
throw new Error(`code of ${name} is empty or undefined`);
|
||||||
}
|
}
|
||||||
ensureNoEmptyLines(name, code);
|
ensureNoEmptyLines(name, code);
|
||||||
ensureCodeHasUniqueLines(name, code);
|
ensureCodeHasUniqueLines(name, code, syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoEmptyLines(name: string, code: string): void {
|
function ensureNoEmptyLines(name: string, code: string): void {
|
||||||
@@ -33,9 +38,9 @@ function ensureNoEmptyLines(name: string, code: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureCodeHasUniqueLines(name: string, code: string): void {
|
function ensureCodeHasUniqueLines(name: string, code: string, syntax: ILanguageSyntax): void {
|
||||||
const lines = code.split('\n')
|
const lines = code.split('\n')
|
||||||
.filter((line) => !shouldIgnoreLine(line));
|
.filter((line) => !shouldIgnoreLine(line, syntax));
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -45,13 +50,12 @@ function ensureCodeHasUniqueLines(name: string, code: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldIgnoreLine(codeLine: string): boolean {
|
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||||
codeLine = codeLine.toLowerCase();
|
codeLine = codeLine.toLowerCase();
|
||||||
const isCommentLine = () => codeLine.startsWith(':: ') || codeLine.startsWith('rem ');
|
const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter));
|
||||||
const consistsOfFrequentCommands = () => {
|
const consistsOfFrequentCommands = () => {
|
||||||
const frequentCodeParts = [ '(', ')', 'else' ];
|
|
||||||
const trimmed = codeLine.trim().split(' ');
|
const trimmed = codeLine.trim().split(' ');
|
||||||
return trimmed.every((part) => frequentCodeParts.includes(part));
|
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
||||||
};
|
};
|
||||||
return isCommentLine() || consistsOfFrequentCommands();
|
return isCommentLine() || consistsOfFrequentCommands();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export class ScriptingDefinition implements IScriptingDefinition {
|
|||||||
|
|
||||||
function findExtension(language: ScriptingLanguage): string {
|
function findExtension(language: ScriptingLanguage): string {
|
||||||
switch (language) {
|
switch (language) {
|
||||||
case ScriptingLanguage.bash:
|
case ScriptingLanguage.shellscript:
|
||||||
return 'sh';
|
return 'sh';
|
||||||
case ScriptingLanguage.batchfile:
|
case ScriptingLanguage.batchfile:
|
||||||
return 'bat';
|
return 'bat';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export enum ScriptingLanguage {
|
export enum ScriptingLanguage {
|
||||||
batchfile = 0,
|
batchfile = 0,
|
||||||
bash = 1,
|
shellscript = 1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { IEventSubscription } from './ISubscription';
|
||||||
export interface ISignal<T> {
|
export interface ISignal<T> {
|
||||||
on(handler: (data: T) => void): void;
|
on(handler: EventHandler<T>): IEventSubscription;
|
||||||
off(handler: (data: T) => void): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventHandler<T> = (data: T) => void;
|
||||||
|
|||||||
3
src/infrastructure/Events/ISubscription.ts
Normal file
3
src/infrastructure/Events/ISubscription.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface IEventSubscription {
|
||||||
|
unsubscribe(): void;
|
||||||
|
}
|
||||||
@@ -1,18 +1,28 @@
|
|||||||
import { ISignal } from './ISignal';
|
import { EventHandler, ISignal } from './ISignal';
|
||||||
export { ISignal };
|
import { IEventSubscription } from './ISubscription';
|
||||||
|
|
||||||
export class Signal<T> implements ISignal<T> {
|
export class Signal<T> implements ISignal<T> {
|
||||||
private handlers: Array<(data: T) => void> = [];
|
private handlers = new Map<number, EventHandler<T>>();
|
||||||
|
|
||||||
public on(handler: (data: T) => void): void {
|
public on(handler: EventHandler<T>): IEventSubscription {
|
||||||
this.handlers.push(handler);
|
const id = this.getUniqueEventHandlerId();
|
||||||
}
|
this.handlers.set(id, handler);
|
||||||
|
return {
|
||||||
public off(handler: (data: T) => void): void {
|
unsubscribe: () => this.handlers.delete(id),
|
||||||
this.handlers = this.handlers.filter((h) => h !== handler);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public notify(data: T) {
|
public notify(data: T) {
|
||||||
this.handlers.slice(0).forEach((h) => h(data));
|
for (const handler of Array.from(this.handlers.values())) {
|
||||||
|
handler(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUniqueEventHandlerId(): number {
|
||||||
|
const id = Math.random();
|
||||||
|
if (this.handlers.has(id)) {
|
||||||
|
return this.getUniqueEventHandlerId();
|
||||||
|
}
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import fileSaver from 'file-saver';
|
|||||||
|
|
||||||
export enum FileType {
|
export enum FileType {
|
||||||
BatchFile,
|
BatchFile,
|
||||||
|
ShellScript,
|
||||||
}
|
}
|
||||||
export class SaveFileDialog {
|
export class SaveFileDialog {
|
||||||
public static saveFile(text: string, fileName: string, type: FileType): void {
|
public static saveFile(text: string, fileName: string, type: FileType): void {
|
||||||
@@ -12,6 +13,7 @@ export class SaveFileDialog {
|
|||||||
// Some browsers (including firefox + IE) require right mime type
|
// Some browsers (including firefox + IE) require right mime type
|
||||||
// otherwise they ignore extension and save the file as text.
|
// otherwise they ignore extension and save the file as text.
|
||||||
[ FileType.BatchFile, 'application/bat' ], // https://en.wikipedia.org/wiki/Batch_file
|
[ FileType.BatchFile, 'application/bat' ], // https://en.wikipedia.org/wiki/Batch_file
|
||||||
|
[ FileType.ShellScript, 'text/x-shellscript' ], // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
|
||||||
]);
|
]);
|
||||||
|
|
||||||
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
||||||
|
|||||||
55
src/presentation/CodeButtons/Code.vue
Normal file
55
src/presentation/CodeButtons/Code.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<span class="code-wrapper">
|
||||||
|
<span class="dollar">$</span>
|
||||||
|
<code><slot></slot></code>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="copy-button"
|
||||||
|
:icon="['fas', 'copy']"
|
||||||
|
@click="copyCode"
|
||||||
|
v-tooltip.top-center="'Copy'"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Code extends Vue {
|
||||||
|
public copyCode(): void {
|
||||||
|
const code = this.$slots.default[0].text;
|
||||||
|
Clipboard.copyText(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
|
||||||
|
.code-wrapper {
|
||||||
|
white-space: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-family: $normal-font;
|
||||||
|
background-color: $slate;
|
||||||
|
color: $light-gray;
|
||||||
|
padding-left: 0.3rem;
|
||||||
|
padding-right: 0.3rem;
|
||||||
|
.dollar {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.copy-button {
|
||||||
|
margin-left: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,11 +8,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Emit } from 'vue-property-decorator';
|
import { Component, Prop, Emit, Vue } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from './StatefulVue';
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class IconButton extends StatefulVue {
|
export default class IconButton extends Vue {
|
||||||
@Prop() public text!: number;
|
@Prop() public text!: number;
|
||||||
@Prop() public iconPrefix!: string;
|
@Prop() public iconPrefix!: string;
|
||||||
@Prop() public iconName!: string;
|
@Prop() public iconName!: string;
|
||||||
@@ -21,7 +20,6 @@ export default class IconButton extends StatefulVue {
|
|||||||
public onClicked() {
|
public onClicked() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
119
src/presentation/CodeButtons/MacOsInstructions.vue
Normal file
119
src/presentation/CodeButtons/MacOsInstructions.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div class="instructions">
|
||||||
|
<!-- <p>
|
||||||
|
Since you're using online version of {{ this.appName }}, you will need to do additional steps after downloading the file to execute your script on macOS:
|
||||||
|
</p> -->
|
||||||
|
<p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<span>Download the file</span>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="'You should be prompted to save the script file now, otherwise try to download it again'"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Open terminal</span>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="'Type Terminal into Spotlight or open from the Applications -> Utilities folder'"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Navigate to the folder where you downloaded the file e.g.:</span>
|
||||||
|
<div>
|
||||||
|
<Code>cd ~/Downloads</Code>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="
|
||||||
|
'Press on Enter/Return key after running the command.<br/>' +
|
||||||
|
'If the file is not downloaded on Downloads folder, change `Downloads` to path where the file is downloaded.<br/>' +
|
||||||
|
'• `cd` will change the current folder.<br/>' +
|
||||||
|
'• `~` is the user home directory.'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Give the file execute permissions:</span>
|
||||||
|
<div>
|
||||||
|
<Code>chmod +x {{ this.fileName }}</Code>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="
|
||||||
|
'Press on Enter/Return key after running the command.<br/>' +
|
||||||
|
'It will make the file executable.'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Execute the file:</span>
|
||||||
|
<div>
|
||||||
|
<Code>./{{ this.fileName }}</Code>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="'Alternatively you can double click on the file'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>If asked, enter your administrator password</span>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="
|
||||||
|
'Press on Enter/Return key after typing your password<br/>' +
|
||||||
|
'Your password will not be shown by default.<br/>' +
|
||||||
|
'Administor privileges are required to configure OS.'"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</p>
|
||||||
|
<!-- <p>
|
||||||
|
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts directly to skip these steps.
|
||||||
|
</p> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop } from 'vue-property-decorator';
|
||||||
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
|
import Code from './Code.vue';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
Code,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class MacOsInstructions extends StatefulVue {
|
||||||
|
@Prop() public fileName: string;
|
||||||
|
public appName = '';
|
||||||
|
public macOsDownloadUrl = '';
|
||||||
|
|
||||||
|
protected initialize(app: IApplication): void {
|
||||||
|
this.appName = app.info.name;
|
||||||
|
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
|
||||||
|
}
|
||||||
|
protected handleCollectionState(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.explanation {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
161
src/presentation/CodeButtons/TheCodeButtons.vue
Normal file
161
src/presentation/CodeButtons/TheCodeButtons.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container" v-if="hasCode">
|
||||||
|
<IconButton
|
||||||
|
:text="this.isDesktopVersion ? 'Save' : 'Download'"
|
||||||
|
v-on:click="saveCodeAsync"
|
||||||
|
icon-prefix="fas"
|
||||||
|
:icon-name="this.isDesktopVersion ? 'save' : 'file-download'">
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
text="Copy"
|
||||||
|
v-on:click="copyCodeAsync"
|
||||||
|
icon-prefix="fas" icon-name="copy">
|
||||||
|
</IconButton>
|
||||||
|
<modal :name="macOsModalName" height="auto" :scrollable="true" :adaptive="true"
|
||||||
|
v-if="this.isMacOsCollection">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal__content">
|
||||||
|
<MacOsInstructions :fileName="this.fileName" />
|
||||||
|
</div>
|
||||||
|
<div class="modal__close-button">
|
||||||
|
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(macOsModalName)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component } from 'vue-property-decorator';
|
||||||
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
|
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||||
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
|
import IconButton from './IconButton.vue';
|
||||||
|
import MacOsInstructions from './MacOsInstructions.vue';
|
||||||
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
|
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
IconButton,
|
||||||
|
MacOsInstructions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class TheCodeButtons extends StatefulVue {
|
||||||
|
public readonly macOsModalName = 'macos-instructions';
|
||||||
|
|
||||||
|
public hasCode = false;
|
||||||
|
public isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
||||||
|
public isMacOsCollection = false;
|
||||||
|
public fileName = '';
|
||||||
|
|
||||||
|
private codeListener: IEventSubscription;
|
||||||
|
|
||||||
|
public async copyCodeAsync() {
|
||||||
|
const code = await this.getCurrentCodeAsync();
|
||||||
|
Clipboard.copyText(code.current);
|
||||||
|
}
|
||||||
|
public async saveCodeAsync() {
|
||||||
|
const context = await this.getCurrentContextAsync();
|
||||||
|
saveCode(this.fileName, context.state);
|
||||||
|
if (this.isMacOsCollection) {
|
||||||
|
this.$modal.show(this.macOsModalName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public destroyed() {
|
||||||
|
if (this.codeListener) {
|
||||||
|
this.codeListener.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initialize(app: IApplication): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||||
|
this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS;
|
||||||
|
this.fileName = buildFileName(newState.collection.scripting);
|
||||||
|
this.react(newState.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCurrentCodeAsync(): Promise<IApplicationCode> {
|
||||||
|
const context = await this.getCurrentContextAsync();
|
||||||
|
const code = context.state.code;
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
private async react(code: IApplicationCode) {
|
||||||
|
this.hasCode = code.current && code.current.length > 0;
|
||||||
|
if (this.codeListener) {
|
||||||
|
this.codeListener.unsubscribe();
|
||||||
|
}
|
||||||
|
this.codeListener = code.changed.on((newCode) => {
|
||||||
|
this.hasCode = newCode && newCode.code.length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCode(fileName: string, state: ICategoryCollectionState) {
|
||||||
|
const content = state.code.current;
|
||||||
|
const type = getType(state.collection.scripting.language);
|
||||||
|
SaveFileDialog.saveFile(content, fileName, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getType(language: ScriptingLanguage) {
|
||||||
|
switch (language) {
|
||||||
|
case ScriptingLanguage.batchfile:
|
||||||
|
return FileType.BatchFile;
|
||||||
|
case ScriptingLanguage.shellscript:
|
||||||
|
return FileType.ShellScript;
|
||||||
|
default:
|
||||||
|
throw new Error('unknown file type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function buildFileName(scripting: IScriptingDefinition) {
|
||||||
|
const fileName = 'privacy-script';
|
||||||
|
if (scripting.fileExtension) {
|
||||||
|
return `${fileName}.${scripting.fileExtension}`;
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.container > * + * {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
font-family: $normal-font;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
width: 100%;
|
||||||
|
margin: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close-button {
|
||||||
|
width: auto;
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-right:0.25em;
|
||||||
|
align-self: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,6 +21,8 @@ import CardListItem from './CardListItem.vue';
|
|||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { hasDirective } from './NonCollapsingDirective';
|
import { hasDirective } from './NonCollapsingDirective';
|
||||||
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -31,9 +33,7 @@ export default class CardList extends StatefulVue {
|
|||||||
public categoryIds: number[] = [];
|
public categoryIds: number[] = [];
|
||||||
public activeCategoryId?: number = null;
|
public activeCategoryId?: number = null;
|
||||||
|
|
||||||
public async mounted() {
|
public created() {
|
||||||
const context = await this.getCurrentContextAsync();
|
|
||||||
this.setCategories(context.collection.actions);
|
|
||||||
this.onOutsideOfActiveCardClicked((element) => {
|
this.onOutsideOfActiveCardClicked((element) => {
|
||||||
if (hasDirective(element)) {
|
if (hasDirective(element)) {
|
||||||
return;
|
return;
|
||||||
@@ -41,15 +41,21 @@ export default class CardList extends StatefulVue {
|
|||||||
this.activeCategoryId = null;
|
this.activeCategoryId = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSelected(categoryId: number, isExpanded: boolean) {
|
public onSelected(categoryId: number, isExpanded: boolean) {
|
||||||
this.activeCategoryId = isExpanded ? categoryId : undefined;
|
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<ICategory>): void {
|
private setCategories(categories: ReadonlyArray<ICategory>): void {
|
||||||
this.categoryIds = categories.map((category) => category.id);
|
this.categoryIds = categories.map((category) => category.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onOutsideOfActiveCardClicked(callback: (clickedElement: Element) => void) {
|
private onOutsideOfActiveCardClicked(callback: (clickedElement: Element) => void) {
|
||||||
const outsideClickListener = (event) => {
|
const outsideClickListener = (event) => {
|
||||||
if (!this.activeCategoryId) {
|
if (!this.activeCategoryId) {
|
||||||
|
|||||||
@@ -49,16 +49,17 @@ export default class CardListItem extends StatefulVue {
|
|||||||
public isAnyChildSelected = false;
|
public isAnyChildSelected = false;
|
||||||
public areAllChildrenSelected = false;
|
public areAllChildrenSelected = false;
|
||||||
|
|
||||||
|
public async mounted() {
|
||||||
|
this.updateStateAsync(this.categoryId);
|
||||||
|
}
|
||||||
@Emit('selected')
|
@Emit('selected')
|
||||||
public onSelected(isExpanded: boolean) {
|
public onSelected(isExpanded: boolean) {
|
||||||
this.isExpanded = isExpanded;
|
this.isExpanded = isExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('activeCategoryId')
|
@Watch('activeCategoryId')
|
||||||
public async onActiveCategoryChanged(value: |number) {
|
public async onActiveCategoryChanged(value: |number) {
|
||||||
this.isExpanded = value === this.categoryId;
|
this.isExpanded = value === this.categoryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('isExpanded')
|
@Watch('isExpanded')
|
||||||
public async onExpansionChangedAsync(newValue: number, oldValue: number) {
|
public async onExpansionChangedAsync(newValue: number, oldValue: number) {
|
||||||
if (!oldValue && newValue) {
|
if (!oldValue && newValue) {
|
||||||
@@ -67,24 +68,22 @@ export default class CardListItem extends StatefulVue {
|
|||||||
(focusElement as HTMLElement).scrollIntoView({behavior: 'smooth'});
|
(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')
|
@Watch('categoryId')
|
||||||
public async updateStateAsync(value: |number) {
|
public async updateStateAsync(value: |number) {
|
||||||
const context = await this.getCurrentContextAsync();
|
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;
|
this.cardTitle = category ? category.name : undefined;
|
||||||
const currentSelection = context.state.selection;
|
const currentSelection = context.state.selection;
|
||||||
this.isAnyChildSelected = category ? currentSelection.isAnySelected(category) : false;
|
this.isAnyChildSelected = category ? currentSelection.isAnySelected(category) : false;
|
||||||
this.areAllChildrenSelected = category ? currentSelection.areAllSelected(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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -15,15 +15,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
|
||||||
import { Grouping } from './Grouping';
|
import { Grouping } from './Grouping';
|
||||||
|
|
||||||
const DefaultGrouping = Grouping.Cards;
|
const DefaultGrouping = Grouping.Cards;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class TheGrouper extends StatefulVue {
|
export default class TheGrouper extends Vue {
|
||||||
|
|
||||||
public cardsSelected = false;
|
public cardsSelected = false;
|
||||||
public noneSelected = false;
|
public noneSelected = false;
|
||||||
|
|
||||||
@@ -32,11 +30,9 @@ export default class TheGrouper extends StatefulVue {
|
|||||||
public mounted() {
|
public mounted() {
|
||||||
this.changeGrouping(DefaultGrouping);
|
this.changeGrouping(DefaultGrouping);
|
||||||
}
|
}
|
||||||
|
|
||||||
public groupByCard() {
|
public groupByCard() {
|
||||||
this.changeGrouping(Grouping.Cards);
|
this.changeGrouping(Grouping.Cards);
|
||||||
}
|
}
|
||||||
|
|
||||||
public groupByNone() {
|
public groupByNone() {
|
||||||
this.changeGrouping(Grouping.None);
|
this.changeGrouping(Grouping.None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,14 @@
|
|||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId, getCategoryId, getScriptId } from './ScriptNodeParser';
|
import { parseAllCategories, parseSingleCategory, getScriptNodeId,
|
||||||
|
getCategoryNodeId, getCategoryId, getScriptId } from './ScriptNodeParser';
|
||||||
import SelectableTree from './SelectableTree/SelectableTree.vue';
|
import SelectableTree from './SelectableTree/SelectableTree.vue';
|
||||||
import { INode, NodeType } from './SelectableTree/Node/INode';
|
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
|
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -40,12 +43,7 @@
|
|||||||
public filterText?: string = null;
|
public filterText?: string = null;
|
||||||
|
|
||||||
private filtered?: IFilterResult;
|
private filtered?: IFilterResult;
|
||||||
|
private listeners = new Array<IEventSubscription>();
|
||||||
public async mounted() {
|
|
||||||
const context = await this.getCurrentContextAsync();
|
|
||||||
this.beginReactingToStateChanges(context.state);
|
|
||||||
this.setInitialState(context.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
||||||
const context = await this.getCurrentContextAsync();
|
const context = await this.getCurrentContextAsync();
|
||||||
@@ -60,54 +58,62 @@
|
|||||||
throw new Error(`Unknown node type: ${event.node.id}`);
|
throw new Error(`Unknown node type: ${event.node.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Watch('categoryId', { immediate: true })
|
||||||
@Watch('categoryId')
|
public async setNodesAsync(categoryId?: number) {
|
||||||
public async initializeNodesAsync(categoryId?: number) {
|
|
||||||
const context = await this.getCurrentContextAsync();
|
const context = await this.getCurrentContextAsync();
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
this.nodes = parseSingleCategory(categoryId, context.collection);
|
this.nodes = parseSingleCategory(categoryId, context.state.collection);
|
||||||
} else {
|
} else {
|
||||||
this.nodes = parseAllCategories(context.collection);
|
this.nodes = parseAllCategories(context.state.collection);
|
||||||
}
|
}
|
||||||
this.selectedNodeIds = context.state.selection.selectedScripts
|
this.selectedNodeIds = context.state.selection.selectedScripts
|
||||||
.map((selected) => getScriptNodeId(selected.script));
|
.map((selected) => getScriptNodeId(selected.script));
|
||||||
}
|
}
|
||||||
|
|
||||||
public filterPredicate(node: INode): boolean {
|
public filterPredicate(node: INode): boolean {
|
||||||
return this.filtered.scriptMatches.some(
|
return this.filtered.scriptMatches.some(
|
||||||
(script: IScript) => node.id === getScriptNodeId(script))
|
(script: IScript) => node.id === getScriptNodeId(script))
|
||||||
|| this.filtered.categoryMatches.some(
|
|| this.filtered.categoryMatches.some(
|
||||||
(category: ICategory) => node.id === getCategoryNodeId(category));
|
(category: ICategory) => node.id === getCategoryNodeId(category));
|
||||||
}
|
}
|
||||||
|
public destroyed() {
|
||||||
private beginReactingToStateChanges(state: ICategoryCollectionState) {
|
this.unsubscribeAll();
|
||||||
state.selection.changed.on(this.handleSelectionChanged);
|
|
||||||
state.filter.filterRemoved.on(this.handleFilterRemoved);
|
|
||||||
state.filter.filtered.on(this.handleFiltered);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setInitialState(state: ICategoryCollectionState) {
|
protected initialize(app: IApplication): void {
|
||||||
this.initializeNodesAsync(this.categoryId);
|
return;
|
||||||
this.initializeFilter(state.filter.currentFilter);
|
}
|
||||||
|
protected async handleCollectionState(newState: ICategoryCollectionState) {
|
||||||
|
this.setCurrentFilter(newState.filter.currentFilter);
|
||||||
|
if (!this.categoryId) {
|
||||||
|
this.nodes = parseAllCategories(newState.collection);
|
||||||
|
}
|
||||||
|
this.unsubscribeAll();
|
||||||
|
this.subscribe(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeFilter(currentFilter: IFilterResult | undefined) {
|
private subscribe(state: ICategoryCollectionState) {
|
||||||
|
this.listeners.push(state.selection.changed.on(this.handleSelectionChanged));
|
||||||
|
this.listeners.push(state.filter.filterRemoved.on(this.handleFilterRemoved));
|
||||||
|
this.listeners.push(state.filter.filtered.on(this.handleFiltered));
|
||||||
|
}
|
||||||
|
private unsubscribeAll() {
|
||||||
|
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||||
|
this.listeners.splice(0, this.listeners.length);
|
||||||
|
}
|
||||||
|
private setCurrentFilter(currentFilter: IFilterResult | undefined) {
|
||||||
if (!currentFilter) {
|
if (!currentFilter) {
|
||||||
this.handleFilterRemoved();
|
this.handleFilterRemoved();
|
||||||
} else {
|
} else {
|
||||||
this.handleFiltered(currentFilter);
|
this.handleFiltered(currentFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
|
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
|
||||||
this.selectedNodeIds = selectedScripts
|
this.selectedNodeIds = selectedScripts
|
||||||
.map((node) => node.id);
|
.map((node) => node.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleFilterRemoved() {
|
private handleFilterRemoved() {
|
||||||
this.filterText = '';
|
this.filterText = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleFiltered(result: IFilterResult) {
|
private handleFiltered(result: IFilterResult) {
|
||||||
this.filterText = result.query;
|
this.filterText = result.query;
|
||||||
this.filtered = result;
|
this.filtered = result;
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
import { INode } from './INode';
|
import { INode } from './INode';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { getReverter } from './Reverter/ReverterFactory';
|
import { getReverter } from './Reverter/ReverterFactory';
|
||||||
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class RevertToggle extends StatefulVue {
|
export default class RevertToggle extends StatefulVue {
|
||||||
@@ -26,26 +29,30 @@
|
|||||||
public isReverted = false;
|
public isReverted = false;
|
||||||
|
|
||||||
private handler: IReverter;
|
private handler: IReverter;
|
||||||
|
private selectionChangeListener: IEventSubscription;
|
||||||
|
|
||||||
public async mounted() {
|
@Watch('node', {immediate: true}) public async onNodeChangedAsync(node: INode) {
|
||||||
await this.onNodeChangedAsync(this.node);
|
|
||||||
const context = await this.getCurrentContextAsync();
|
const context = await this.getCurrentContextAsync();
|
||||||
const currentSelection = context.state.selection;
|
this.handler = getReverter(node, context.state.collection);
|
||||||
this.updateState(currentSelection.selectedScripts);
|
|
||||||
currentSelection.changed.on((scripts) => this.updateState(scripts));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('node') public async onNodeChangedAsync(node: INode) {
|
|
||||||
const context = await this.getCurrentContextAsync();
|
|
||||||
this.handler = getReverter(node, context.collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async onRevertToggledAsync() {
|
public async onRevertToggledAsync() {
|
||||||
const context = await this.getCurrentContextAsync();
|
const context = await this.getCurrentContextAsync();
|
||||||
this.handler.selectWithRevertState(this.isReverted, context.state.selection);
|
this.handler.selectWithRevertState(this.isReverted, context.state.selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateState(scripts: ReadonlyArray<SelectedScript>) {
|
protected initialize(app: IApplication): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||||
|
this.updateStatus(newState.selection.selectedScripts);
|
||||||
|
if (this.selectionChangeListener) {
|
||||||
|
this.selectionChangeListener.unsubscribe();
|
||||||
|
}
|
||||||
|
this.selectionChangeListener = newState.selection.changed.on(
|
||||||
|
(scripts) => this.updateStatus(scripts));
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateStatus(scripts: ReadonlyArray<SelectedScript>) {
|
||||||
this.isReverted = this.handler.getState(scripts);
|
this.isReverted = this.handler.getState(scripts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IUserSelection } from '@/application/Context/State/ICategoryCollectionState';
|
|
||||||
|
|
||||||
export interface IReverter {
|
export interface IReverter {
|
||||||
getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean;
|
getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IReverter } from './IReverter';
|
import { IReverter } from './IReverter';
|
||||||
import { getScriptId } from '../../../ScriptNodeParser';
|
import { getScriptId } from '../../../ScriptNodeParser';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IUserSelection } from '@/application/Context/State/ICategoryCollectionState';
|
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||||
|
|
||||||
export class ScriptReverter implements IReverter {
|
export class ScriptReverter implements IReverter {
|
||||||
private readonly scriptId: string;
|
private readonly scriptId: string;
|
||||||
|
|||||||
@@ -45,22 +45,6 @@
|
|||||||
public liquorTreeOptions = new LiquorTreeOptions(new NodePredicateFilter((node) => this.filterPredicate(node)));
|
public liquorTreeOptions = new LiquorTreeOptions(new NodePredicateFilter((node) => this.filterPredicate(node)));
|
||||||
public convertExistingToNode = convertExistingToNode;
|
public convertExistingToNode = convertExistingToNode;
|
||||||
|
|
||||||
public mounted() {
|
|
||||||
if (this.initialNodes) {
|
|
||||||
const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node));
|
|
||||||
if (this.selectedNodeIds) {
|
|
||||||
recurseDown(initialNodes,
|
|
||||||
(node) => node.state = updateState(node.state, node, this.selectedNodeIds));
|
|
||||||
}
|
|
||||||
this.initialLiquourTreeNodes = initialNodes;
|
|
||||||
} else {
|
|
||||||
throw new Error('Initial nodes are null or empty');
|
|
||||||
}
|
|
||||||
if (this.filterText) {
|
|
||||||
this.updateFilterText(this.filterText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public nodeSelected(node: ILiquorTreeExistingNode) {
|
public nodeSelected(node: ILiquorTreeExistingNode) {
|
||||||
const event: INodeSelectedEvent = {
|
const event: INodeSelectedEvent = {
|
||||||
node: convertExistingToNode(node),
|
node: convertExistingToNode(node),
|
||||||
@@ -70,9 +54,23 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('filterText')
|
@Watch('initialNodes', { immediate: true })
|
||||||
public updateFilterText(filterText: |string) {
|
public async updateNodesAsync(nodes: readonly INode[]) {
|
||||||
const api = this.getLiquorTreeApi();
|
if (!nodes) {
|
||||||
|
throw new Error('undefined initial nodes');
|
||||||
|
}
|
||||||
|
const initialNodes = nodes.map((node) => toNewLiquorTreeNode(node));
|
||||||
|
if (this.selectedNodeIds) {
|
||||||
|
recurseDown(initialNodes,
|
||||||
|
(node) => node.state = updateState(node.state, node, this.selectedNodeIds));
|
||||||
|
}
|
||||||
|
this.initialLiquourTreeNodes = initialNodes;
|
||||||
|
const api = await this.getLiquorTreeApiAsync();
|
||||||
|
api.setModel(this.initialLiquourTreeNodes); // as liquor tree is not reactive to data after initialization
|
||||||
|
}
|
||||||
|
@Watch('filterText', { immediate: true })
|
||||||
|
public async updateFilterTextAsync(filterText: |string) {
|
||||||
|
const api = await this.getLiquorTreeApiAsync();
|
||||||
if (!filterText) {
|
if (!filterText) {
|
||||||
api.clearFilter();
|
api.clearFilter();
|
||||||
} else {
|
} else {
|
||||||
@@ -81,20 +79,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Watch('selectedNodeIds')
|
@Watch('selectedNodeIds')
|
||||||
public setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) {
|
public async setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) {
|
||||||
if (!selectedNodeIds) {
|
if (!selectedNodeIds) {
|
||||||
throw new Error('SelectedrecurseDown nodes are undefined');
|
throw new Error('SelectedrecurseDown nodes are undefined');
|
||||||
}
|
}
|
||||||
this.getLiquorTreeApi().recurseDown(
|
const tree = await this.getLiquorTreeApiAsync();
|
||||||
|
tree.recurseDown(
|
||||||
(node) => node.states = updateState(node.states, node, selectedNodeIds),
|
(node) => node.states = updateState(node.states, node, selectedNodeIds),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getLiquorTreeApi(): ILiquorTree {
|
private async getLiquorTreeApiAsync(): Promise<ILiquorTree> {
|
||||||
if (!this.$refs.treeElement) {
|
const accessor = (): ILiquorTree => {
|
||||||
throw new Error('Referenced tree element cannot be found. Probably it\'s not rendered?');
|
const uiElement = this.$refs.treeElement;
|
||||||
|
return uiElement ? (uiElement as any).tree : undefined;
|
||||||
|
};
|
||||||
|
const treeElement = await tryUntilDefinedAsync(accessor, 5, 20); // Wait for it to render
|
||||||
|
if (!treeElement) {
|
||||||
|
throw Error('Referenced tree element cannot be found. Perhaps it\'s not yet rendered?');
|
||||||
}
|
}
|
||||||
return (this.$refs.treeElement as any).tree;
|
return treeElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +108,6 @@
|
|||||||
selectedNodeIds: ReadonlyArray<string>): ILiquorTreeNodeState {
|
selectedNodeIds: ReadonlyArray<string>): ILiquorTreeNodeState {
|
||||||
return {...old, ...getNewState(node, selectedNodeIds)};
|
return {...old, ...getNewState(node, selectedNodeIds)};
|
||||||
}
|
}
|
||||||
|
|
||||||
function recurseDown(
|
function recurseDown(
|
||||||
nodes: ReadonlyArray<ILiquorTreeNewNode>,
|
nodes: ReadonlyArray<ILiquorTreeNewNode>,
|
||||||
handler: (node: ILiquorTreeNewNode) => void) {
|
handler: (node: ILiquorTreeNewNode) => void) {
|
||||||
@@ -115,4 +118,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function tryUntilDefinedAsync<T>(
|
||||||
|
accessor: () => T | undefined,
|
||||||
|
delayInMs: number, maxTries: number): Promise<T | undefined> {
|
||||||
|
const sleepAsync = () => new Promise(((resolve) => setTimeout(resolve, delayInMs)));
|
||||||
|
let triesLeft = maxTries;
|
||||||
|
let value: T;
|
||||||
|
while (triesLeft !== 0) {
|
||||||
|
value = accessor();
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
triesLeft--;
|
||||||
|
await sleepAsync();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,14 +6,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Emit } from 'vue-property-decorator';
|
import { Component, Prop, Emit, Vue } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
|
||||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
directives: { NonCollapsing },
|
directives: { NonCollapsing },
|
||||||
})
|
})
|
||||||
export default class SelectableOption extends StatefulVue {
|
export default class SelectableOption extends Vue {
|
||||||
@Prop() public enabled: boolean;
|
@Prop() public enabled: boolean;
|
||||||
@Prop() public label: string;
|
@Prop() public label: string;
|
||||||
@Emit('click') public onClicked() { return; }
|
@Emit('click') public onClicked() { return; }
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ import SelectableOption from './SelectableOption.vue';
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
enum SelectionState {
|
enum SelectionState {
|
||||||
Standard,
|
Standard,
|
||||||
@@ -66,80 +67,80 @@ export default class TheSelector extends StatefulVue {
|
|||||||
public SelectionState = SelectionState;
|
public SelectionState = SelectionState;
|
||||||
public currentSelection = SelectionState.None;
|
public currentSelection = SelectionState.None;
|
||||||
|
|
||||||
public async mounted() {
|
|
||||||
const context = await this.getCurrentContextAsync();
|
|
||||||
this.updateSelections(context);
|
|
||||||
this.beginReactingToChanges(context);
|
|
||||||
}
|
|
||||||
public async selectAsync(type: SelectionState): Promise<void> {
|
public async selectAsync(type: SelectionState): Promise<void> {
|
||||||
if (this.currentSelection === type) {
|
if (this.currentSelection === type) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const context = await this.getCurrentContextAsync();
|
const context = await this.getCurrentContextAsync();
|
||||||
selectType(context, type);
|
selectType(context.state, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSelections(context: IApplicationContext) {
|
protected initialize(app: IApplication): void {
|
||||||
this.currentSelection = getCurrentSelectionState(context);
|
return;
|
||||||
|
}
|
||||||
|
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||||
|
this.updateSelections(newState);
|
||||||
|
newState.selection.changed.on(() => this.updateSelections(newState));
|
||||||
|
if (oldState) {
|
||||||
|
oldState.selection.changed.on(() => this.updateSelections(oldState));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private beginReactingToChanges(context: IApplicationContext) {
|
private updateSelections(state: ICategoryCollectionState) {
|
||||||
context.state.selection.changed.on(() => {
|
this.currentSelection = getCurrentSelectionState(state);
|
||||||
this.updateSelections(context);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITypeSelector {
|
interface ITypeSelector {
|
||||||
isSelected: (context: IApplicationContext) => boolean;
|
isSelected: (state: ICategoryCollectionState) => boolean;
|
||||||
select: (context: IApplicationContext) => void;
|
select: (state: ICategoryCollectionState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectors = new Map<SelectionState, ITypeSelector>([
|
const selectors = new Map<SelectionState, ITypeSelector>([
|
||||||
[SelectionState.None, {
|
[SelectionState.None, {
|
||||||
select: (context) =>
|
select: (state) =>
|
||||||
context.state.selection.deselectAll(),
|
state.selection.deselectAll(),
|
||||||
isSelected: (context) =>
|
isSelected: (state) =>
|
||||||
context.state.selection.totalSelected === 0,
|
state.selection.totalSelected === 0,
|
||||||
}],
|
}],
|
||||||
[SelectionState.Standard, {
|
[SelectionState.Standard, {
|
||||||
select: (context) =>
|
select: (state) =>
|
||||||
context.state.selection.selectOnly(
|
state.selection.selectOnly(
|
||||||
context.collection.getScriptsByLevel(RecommendationLevel.Standard)),
|
state.collection.getScriptsByLevel(RecommendationLevel.Standard)),
|
||||||
isSelected: (context) =>
|
isSelected: (state) =>
|
||||||
hasAllSelectedLevelOf(RecommendationLevel.Standard, context),
|
hasAllSelectedLevelOf(RecommendationLevel.Standard, state),
|
||||||
}],
|
}],
|
||||||
[SelectionState.Strict, {
|
[SelectionState.Strict, {
|
||||||
select: (context) =>
|
select: (state) =>
|
||||||
context.state.selection.selectOnly(context.collection.getScriptsByLevel(RecommendationLevel.Strict)),
|
state.selection.selectOnly(state.collection.getScriptsByLevel(RecommendationLevel.Strict)),
|
||||||
isSelected: (context) =>
|
isSelected: (state) =>
|
||||||
hasAllSelectedLevelOf(RecommendationLevel.Strict, context),
|
hasAllSelectedLevelOf(RecommendationLevel.Strict, state),
|
||||||
}],
|
}],
|
||||||
[SelectionState.All, {
|
[SelectionState.All, {
|
||||||
select: (context) =>
|
select: (state) =>
|
||||||
context.state.selection.selectAll(),
|
state.selection.selectAll(),
|
||||||
isSelected: (context) =>
|
isSelected: (state) =>
|
||||||
context.state.selection.totalSelected === context.collection.totalScripts,
|
state.selection.totalSelected === state.collection.totalScripts,
|
||||||
}],
|
}],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function selectType(context: IApplicationContext, type: SelectionState) {
|
function selectType(state: ICategoryCollectionState, type: SelectionState) {
|
||||||
const selector = selectors.get(type);
|
const selector = selectors.get(type);
|
||||||
selector.select(context);
|
selector.select(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentSelectionState(context: IApplicationContext): SelectionState {
|
function getCurrentSelectionState(state: ICategoryCollectionState): SelectionState {
|
||||||
for (const [type, selector] of Array.from(selectors.entries())) {
|
for (const [type, selector] of Array.from(selectors.entries())) {
|
||||||
if (selector.isSelected(context)) {
|
if (selector.isSelected(state)) {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SelectionState.Custom;
|
return SelectionState.Custom;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasAllSelectedLevelOf(level: RecommendationLevel, context: IApplicationContext) {
|
function hasAllSelectedLevelOf(level: RecommendationLevel, state: ICategoryCollectionState) {
|
||||||
const scripts = context.collection.getScriptsByLevel(level);
|
const scripts = state.collection.getScriptsByLevel(level);
|
||||||
const selectedScripts = context.state.selection.selectedScripts;
|
const selectedScripts = state.selection.selectedScripts;
|
||||||
return areAllSelected(scripts, selectedScripts);
|
return areAllSelected(scripts, selectedScripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
74
src/presentation/Scripts/TheOsChanger.vue
Normal file
74
src/presentation/Scripts/TheOsChanger.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div v-for="os in this.allOses" :key="os.name">
|
||||||
|
<span
|
||||||
|
class="name"
|
||||||
|
v-bind:class="{ 'current': currentOs === os.os }"
|
||||||
|
v-on:click="changeOsAsync(os.os)">
|
||||||
|
{{ os.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component } from 'vue-property-decorator';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class TheOsChanger extends StatefulVue {
|
||||||
|
public allOses: Array<{ name: string, os: OperatingSystem }> = [];
|
||||||
|
public currentOs: OperatingSystem = undefined;
|
||||||
|
|
||||||
|
public async changeOsAsync(newOs: OperatingSystem) {
|
||||||
|
const context = await this.getCurrentContextAsync();
|
||||||
|
context.changeContext(newOs);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initialize(app: IApplication): void {
|
||||||
|
this.allOses = app.getSupportedOsList()
|
||||||
|
.map((os) => ({ os, name: renderOsName(os) }));
|
||||||
|
}
|
||||||
|
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||||
|
this.currentOs = newState.os;
|
||||||
|
this.$forceUpdate(); // v-bind:class is not updated otherwise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOsName(os: OperatingSystem): string {
|
||||||
|
switch (os) {
|
||||||
|
case OperatingSystem.Windows: return 'Windows';
|
||||||
|
case OperatingSystem.macOS: return 'macOS (preview)';
|
||||||
|
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
.container {
|
||||||
|
font-family: $normal-font;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
div + div::before {
|
||||||
|
content: "|";
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
&:not(.current) {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.current {
|
||||||
|
color: $gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="help-container">
|
<div class="heading">
|
||||||
<TheSelector />
|
<TheSelector class="item"/>
|
||||||
|
<TheOsChanger class="item"/>
|
||||||
<TheGrouper
|
<TheGrouper
|
||||||
|
class="item"
|
||||||
v-on:groupingChanged="onGroupingChanged($event)"
|
v-on:groupingChanged="onGroupingChanged($event)"
|
||||||
v-show="!this.isSearching" />
|
v-if="!this.isSearching" />
|
||||||
</div>
|
</div>
|
||||||
<div class="scripts">
|
<div class="scripts">
|
||||||
<div v-if="!isSearching">
|
<div v-if="!isSearching">
|
||||||
@@ -37,14 +39,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import TheGrouper from '@/presentation/Scripts/Grouping/TheGrouper.vue';
|
||||||
|
import TheOsChanger from '@/presentation/Scripts/TheOsChanger.vue';
|
||||||
|
import TheSelector from '@/presentation/Scripts/Selector/TheSelector.vue';
|
||||||
|
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||||
|
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
||||||
import { Component } from 'vue-property-decorator';
|
import { Component } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { Grouping } from './Grouping/Grouping';
|
import { Grouping } from './Grouping/Grouping';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import TheGrouper from '@/presentation/Scripts/Grouping/TheGrouper.vue';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import TheSelector from '@/presentation/Scripts/Selector/TheSelector.vue';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||||
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
|
||||||
|
|
||||||
/** Shows content of single category or many categories */
|
/** Shows content of single category or many categories */
|
||||||
@Component({
|
@Component({
|
||||||
@@ -53,6 +59,7 @@
|
|||||||
TheSelector,
|
TheSelector,
|
||||||
ScriptsTree,
|
ScriptsTree,
|
||||||
CardList,
|
CardList,
|
||||||
|
TheOsChanger,
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
threeDotsTrim(query: string) {
|
threeDotsTrim(query: string) {
|
||||||
@@ -72,37 +79,53 @@
|
|||||||
public isSearching = false;
|
public isSearching = false;
|
||||||
public searchHasMatches = false;
|
public searchHasMatches = false;
|
||||||
|
|
||||||
public async mounted() {
|
private listeners = new Array<IEventSubscription>();
|
||||||
const context = await this.getCurrentContextAsync();
|
|
||||||
this.repositoryUrl = context.app.info.repositoryWebUrl;
|
|
||||||
const filter = context.state.filter;
|
|
||||||
filter.filterRemoved.on(() => {
|
|
||||||
this.isSearching = false;
|
|
||||||
});
|
|
||||||
filter.filtered.on((result: IFilterResult) => {
|
|
||||||
this.searchQuery = result.query;
|
|
||||||
this.isSearching = true;
|
|
||||||
this.searchHasMatches = result.hasAnyMatches();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public destroyed() {
|
||||||
|
this.unsubscribeAll();
|
||||||
|
}
|
||||||
public async clearSearchQueryAsync() {
|
public async clearSearchQueryAsync() {
|
||||||
const context = await this.getCurrentContextAsync();
|
const context = await this.getCurrentContextAsync();
|
||||||
const filter = context.state.filter;
|
const filter = context.state.filter;
|
||||||
filter.removeFilter();
|
filter.removeFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onGroupingChanged(group: Grouping) {
|
public onGroupingChanged(group: Grouping) {
|
||||||
this.currentGrouping = group;
|
this.currentGrouping = group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected initialize(app: IApplication): void {
|
||||||
|
this.repositoryUrl = app.info.repositoryWebUrl;
|
||||||
|
}
|
||||||
|
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||||
|
this.unsubscribeAll();
|
||||||
|
this.subscribe(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribe(state: ICategoryCollectionState) {
|
||||||
|
this.listeners.push(state.filter.filterRemoved.on(() => {
|
||||||
|
this.isSearching = false;
|
||||||
|
}));
|
||||||
|
state.filter.filtered.on((result: IFilterResult) => {
|
||||||
|
this.searchQuery = result.query;
|
||||||
|
this.isSearching = true;
|
||||||
|
this.searchHasMatches = result.hasAnyMatches();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private unsubscribeAll() {
|
||||||
|
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||||
|
this.listeners.splice(0, this.listeners.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/presentation/styles/colors.scss";
|
@import "@/presentation/styles/colors.scss";
|
||||||
@import "@/presentation/styles/fonts.scss";
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
|
||||||
|
$inner-margin: 4px;
|
||||||
|
|
||||||
.scripts {
|
.scripts {
|
||||||
margin-top:10px;
|
margin-top: $inner-margin;
|
||||||
.tree {
|
.tree {
|
||||||
padding-left: 3%;
|
padding-left: 3%;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
@@ -151,9 +174,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-container {
|
.heading {
|
||||||
|
margin-top: $inner-margin;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
.item {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 5px 0 5px;
|
||||||
|
&:first-child {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,13 +1,40 @@
|
|||||||
import { Vue } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { buildContext } from '@/application/Context/ApplicationContextProvider';
|
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 {
|
export abstract class StatefulVue extends Vue {
|
||||||
private static instance = new AsyncLazy<IApplicationContext>(
|
public static instance = new AsyncLazy<IApplicationContext>(
|
||||||
() => Promise.resolve(buildContext()));
|
() => 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<IApplicationContext> {
|
protected getCurrentContextAsync(): Promise<IApplicationContext> {
|
||||||
return StatefulVue.instance.getValueAsync();
|
return StatefulVue.instance.getValueAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleStateChangedEvent(event: IApplicationContextChangedEvent) {
|
||||||
|
this.handleCollectionState(event.newState, event.oldState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,27 +7,14 @@ import { Component, Prop } from 'vue-property-decorator';
|
|||||||
import { StatefulVue } from './StatefulVue';
|
import { StatefulVue } from './StatefulVue';
|
||||||
import ace from 'ace-builds';
|
import ace from 'ace-builds';
|
||||||
import 'ace-builds/webpack-resolver';
|
import 'ace-builds/webpack-resolver';
|
||||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
|
||||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
const NothingChosenCode =
|
import { IApplication } from '@/domain/IApplication';
|
||||||
new CodeBuilder()
|
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||||
.appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows')
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
.appendLine()
|
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
|
||||||
.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();
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class TheCodeArea extends StatefulVue {
|
export default class TheCodeArea extends StatefulVue {
|
||||||
@@ -35,21 +22,41 @@ export default class TheCodeArea extends StatefulVue {
|
|||||||
|
|
||||||
private editor!: ace.Ace.Editor;
|
private editor!: ace.Ace.Editor;
|
||||||
private currentMarkerId?: number;
|
private currentMarkerId?: number;
|
||||||
|
private codeListener: IEventSubscription;
|
||||||
|
|
||||||
@Prop() private theme!: string;
|
@Prop() private theme!: string;
|
||||||
|
|
||||||
public async mounted() {
|
public destroyed() {
|
||||||
const context = await this.getCurrentContextAsync();
|
this.unsubscribeCodeListening();
|
||||||
this.editor = initializeEditor(this.theme, this.editorId, context.collection.scripting.language);
|
this.destroyEditor();
|
||||||
const appCode = context.state.code;
|
|
||||||
this.editor.setValue(appCode.current || NothingChosenCode, 1);
|
|
||||||
appCode.changed.on((code) => this.updateCode(code));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
this.removeCurrentHighlighting();
|
||||||
if (event.isEmpty()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
this.editor.setValue(event.code, 1);
|
this.editor.setValue(event.code, 1);
|
||||||
@@ -60,7 +67,6 @@ export default class TheCodeArea extends StatefulVue {
|
|||||||
this.reactToChanges(event, event.changedScripts);
|
this.reactToChanges(event, event.changedScripts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) {
|
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) {
|
||||||
const positions = scripts
|
const positions = scripts
|
||||||
.map((script) => event.getScriptPositionInCode(script));
|
.map((script) => event.getScriptPositionInCode(script));
|
||||||
@@ -73,19 +79,16 @@ export default class TheCodeArea extends StatefulVue {
|
|||||||
this.scrollToLine(end + 2);
|
this.scrollToLine(end + 2);
|
||||||
this.highlight(start, end);
|
this.highlight(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
private highlight(startRow: number, endRow: number) {
|
private highlight(startRow: number, endRow: number) {
|
||||||
const AceRange = ace.require('ace/range').Range;
|
const AceRange = ace.require('ace/range').Range;
|
||||||
this.currentMarkerId = this.editor.session.addMarker(
|
this.currentMarkerId = this.editor.session.addMarker(
|
||||||
new AceRange(startRow, 0, endRow, 0), 'code-area__highlight', 'fullLine',
|
new AceRange(startRow, 0, endRow, 0), 'code-area__highlight', 'fullLine',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private scrollToLine(row: number) {
|
private scrollToLine(row: number) {
|
||||||
const column = this.editor.session.getLine(row).length;
|
const column = this.editor.session.getLine(row).length;
|
||||||
this.editor.gotoLine(row, column, true);
|
this.editor.gotoLine(row, column, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeCurrentHighlighting() {
|
private removeCurrentHighlighting() {
|
||||||
if (!this.currentMarkerId) {
|
if (!this.currentMarkerId) {
|
||||||
return;
|
return;
|
||||||
@@ -93,6 +96,11 @@ export default class TheCodeArea extends StatefulVue {
|
|||||||
this.editor.session.removeMarker(this.currentMarkerId);
|
this.editor.session.removeMarker(this.currentMarkerId);
|
||||||
this.currentMarkerId = undefined;
|
this.currentMarkerId = undefined;
|
||||||
}
|
}
|
||||||
|
private destroyEditor() {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeEditor(theme: string, editorId: string, language: ScriptingLanguage): ace.Ace.Editor {
|
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) {
|
function getLanguage(language: ScriptingLanguage) {
|
||||||
switch (language) {
|
switch (language) {
|
||||||
case ScriptingLanguage.batchfile:
|
case ScriptingLanguage.batchfile: return 'batchfile';
|
||||||
return 'batchfile';
|
case ScriptingLanguage.shellscript: return 'sh';
|
||||||
default:
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container" v-if="hasCode">
|
|
||||||
<IconButton
|
|
||||||
:text="this.isDesktop ? 'Save' : 'Download'"
|
|
||||||
v-on:click="saveCodeAsync"
|
|
||||||
icon-prefix="fas"
|
|
||||||
:icon-name="this.isDesktop ? 'save' : 'file-download'">
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
text="Copy"
|
|
||||||
v-on:click="copyCodeAsync"
|
|
||||||
icon-prefix="fas" icon-name="copy">
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component } from 'vue-property-decorator';
|
|
||||||
import { StatefulVue } from './StatefulVue';
|
|
||||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
|
||||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
|
||||||
import IconButton from './IconButton.vue';
|
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
|
||||||
import { IApplicationCode } from '@/application/Context/State/ICategoryCollectionState';
|
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
IconButton,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class TheCodeButtons extends StatefulVue {
|
|
||||||
public hasCode = false;
|
|
||||||
public isDesktop = false;
|
|
||||||
|
|
||||||
public async mounted() {
|
|
||||||
const code = await this.getCurrentCodeAsync();
|
|
||||||
this.hasCode = code.current && code.current.length > 0;
|
|
||||||
code.changed.on((newCode) => {
|
|
||||||
this.hasCode = newCode && newCode.code.length > 0;
|
|
||||||
});
|
|
||||||
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async copyCodeAsync() {
|
|
||||||
const code = await this.getCurrentCodeAsync();
|
|
||||||
Clipboard.copyText(code.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async saveCodeAsync() {
|
|
||||||
const context = await this.getCurrentContextAsync();
|
|
||||||
saveCode(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getCurrentCodeAsync(): Promise<IApplicationCode> {
|
|
||||||
const context = await this.getCurrentContextAsync();
|
|
||||||
const code = context.state.code;
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveCode(context: IApplicationContext) {
|
|
||||||
const fileName = `privacy-script.${context.collection.scripting.fileExtension}`;
|
|
||||||
const content = context.state.code.current;
|
|
||||||
const type = getType(context.collection.scripting.language);
|
|
||||||
SaveFileDialog.saveFile(content, fileName, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getType(language: ScriptingLanguage) {
|
|
||||||
switch (language) {
|
|
||||||
case ScriptingLanguage.batchfile:
|
|
||||||
return FileType.BatchFile;
|
|
||||||
default:
|
|
||||||
throw new Error('unknown file type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/presentation/styles/colors.scss";
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.container > * + * {
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -13,11 +13,12 @@ import { Component, Prop, Watch } from 'vue-property-decorator';
|
|||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class DownloadUrlListItem extends StatefulVue {
|
export default class DownloadUrlListItem extends StatefulVue {
|
||||||
@Prop() public operatingSystem!: OperatingSystem;
|
@Prop() public operatingSystem!: OperatingSystem;
|
||||||
public OperatingSystem = OperatingSystem;
|
|
||||||
|
|
||||||
public downloadUrl: string = '';
|
public downloadUrl: string = '';
|
||||||
public operatingSystemName: string = '';
|
public operatingSystemName: string = '';
|
||||||
@@ -37,6 +38,13 @@ export default class DownloadUrlListItem extends StatefulVue {
|
|||||||
this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs);
|
this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected initialize(app: IApplication): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
|
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
|
||||||
const context = await this.getCurrentContextAsync();
|
const context = await this.getCurrentContextAsync();
|
||||||
return context.app.info.getDownloadUrl(os);
|
return context.app.info.getDownloadUrl(os);
|
||||||
|
|||||||
@@ -34,24 +34,22 @@
|
|||||||
import { Component } from 'vue-property-decorator';
|
import { Component } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class TheFooter extends StatefulVue {
|
export default class PrivacyPolicy extends StatefulVue {
|
||||||
public repositoryUrl: string = '';
|
public repositoryUrl: string = '';
|
||||||
public feedbackUrl: string = '';
|
public feedbackUrl: string = '';
|
||||||
public isDesktop: boolean = false;
|
public isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||||
|
|
||||||
constructor() {
|
protected initialize(app: IApplication): void {
|
||||||
super();
|
const info = app.info;
|
||||||
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async mounted() {
|
|
||||||
const context = await this.getCurrentContextAsync();
|
|
||||||
const info = context.app.info;
|
|
||||||
this.repositoryUrl = info.repositoryWebUrl;
|
this.repositoryUrl = info.repositoryWebUrl;
|
||||||
this.feedbackUrl = info.feedbackUrl;
|
this.feedbackUrl = info.feedbackUrl;
|
||||||
}
|
}
|
||||||
|
protected handleCollectionState(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ import { StatefulVue } from '@/presentation/StatefulVue';
|
|||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||||
import DownloadUrlList from './DownloadUrlList.vue';
|
import DownloadUrlList from './DownloadUrlList.vue';
|
||||||
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -73,15 +75,18 @@ export default class TheFooter extends StatefulVue {
|
|||||||
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
|
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async mounted() {
|
protected initialize(app: IApplication): void {
|
||||||
const context = await this.getCurrentContextAsync();
|
const info = app.info;
|
||||||
const info = context.app.info;
|
|
||||||
this.version = info.version;
|
this.version = info.version;
|
||||||
this.homepageUrl = info.homepage;
|
this.homepageUrl = info.homepage;
|
||||||
this.repositoryUrl = info.repositoryWebUrl;
|
this.repositoryUrl = info.repositoryWebUrl;
|
||||||
this.releaseUrl = info.releaseUrl;
|
this.releaseUrl = info.releaseUrl;
|
||||||
this.feedbackUrl = info.feedbackUrl;
|
this.feedbackUrl = info.feedbackUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<h1 class="child title" >{{ title }}</h1>
|
<h1 class="child title" >{{ title }}</h1>
|
||||||
<h2 class="child subtitle">Enforce privacy & security on Windows</h2>
|
<h2 class="child subtitle">Enforce privacy & security on Windows and macOS</h2>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { Component } from 'vue-property-decorator';
|
import { Component } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from './StatefulVue';
|
import { StatefulVue } from './StatefulVue';
|
||||||
|
|
||||||
@@ -14,9 +16,11 @@ export default class TheHeader extends StatefulVue {
|
|||||||
public title = '';
|
public title = '';
|
||||||
public subtitle = '';
|
public subtitle = '';
|
||||||
|
|
||||||
public async mounted() {
|
protected initialize(app: IApplication): void {
|
||||||
const context = await this.getCurrentContextAsync();
|
this.title = app.info.name;
|
||||||
this.title = context.app.info.name;
|
}
|
||||||
|
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,7 +13,11 @@
|
|||||||
import { Component, Watch } from 'vue-property-decorator';
|
import { Component, Watch } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from './StatefulVue';
|
import { StatefulVue } from './StatefulVue';
|
||||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
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( {
|
@Component( {
|
||||||
directives: { NonCollapsing },
|
directives: { NonCollapsing },
|
||||||
@@ -23,12 +27,7 @@ export default class TheSearchBar extends StatefulVue {
|
|||||||
public searchPlaceHolder = 'Search';
|
public searchPlaceHolder = 'Search';
|
||||||
public searchQuery = '';
|
public searchQuery = '';
|
||||||
|
|
||||||
public async mounted() {
|
private readonly listeners = new Array<IEventSubscription>();
|
||||||
const context = await this.getCurrentContextAsync();
|
|
||||||
const totalScripts = context.collection.totalScripts;
|
|
||||||
this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
|
|
||||||
this.beginReacting(context.state.filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('searchQuery')
|
@Watch('searchQuery')
|
||||||
public async updateFilterAsync(newFilter: |string) {
|
public async updateFilterAsync(newFilter: |string) {
|
||||||
@@ -40,10 +39,34 @@ export default class TheSearchBar extends StatefulVue {
|
|||||||
filter.setFilter(newFilter);
|
filter.setFilter(newFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public destroyed() {
|
||||||
|
this.unsubscribeAll();
|
||||||
|
}
|
||||||
|
|
||||||
private beginReacting(filter: IUserFilter) {
|
protected initialize(app: IApplication): void {
|
||||||
filter.filtered.on((result) => this.searchQuery = result.query);
|
return;
|
||||||
filter.filterRemoved.on(() => this.searchQuery = '');
|
}
|
||||||
|
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 = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('ApplicationContext', () => {
|
|||||||
.construct();
|
.construct();
|
||||||
sut.changeContext(OperatingSystem.macOS);
|
sut.changeContext(OperatingSystem.macOS);
|
||||||
// assert
|
// assert
|
||||||
expect(sut.collection).to.equal(expectedCollection);
|
expect(sut.state.collection).to.equal(expectedCollection);
|
||||||
});
|
});
|
||||||
it('currentOs is changed as expected', () => {
|
it('currentOs is changed as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -35,9 +35,9 @@ describe('ApplicationContext', () => {
|
|||||||
.construct();
|
.construct();
|
||||||
sut.changeContext(expectedOs);
|
sut.changeContext(expectedOs);
|
||||||
// assert
|
// 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
|
// arrange
|
||||||
const testContext = new ObservableApplicationContextFactory()
|
const testContext = new ObservableApplicationContextFactory()
|
||||||
.withAppContainingCollections(OperatingSystem.Windows, OperatingSystem.macOS);
|
.withAppContainingCollections(OperatingSystem.Windows, OperatingSystem.macOS);
|
||||||
@@ -45,6 +45,7 @@ describe('ApplicationContext', () => {
|
|||||||
const sut = testContext
|
const sut = testContext
|
||||||
.withInitialOs(OperatingSystem.Windows)
|
.withInitialOs(OperatingSystem.Windows)
|
||||||
.construct();
|
.construct();
|
||||||
|
sut.state.filter.setFilter('filtered');
|
||||||
sut.changeContext(OperatingSystem.macOS);
|
sut.changeContext(OperatingSystem.macOS);
|
||||||
// assert
|
// assert
|
||||||
expectEmptyState(sut.state);
|
expectEmptyState(sut.state);
|
||||||
@@ -82,12 +83,13 @@ describe('ApplicationContext', () => {
|
|||||||
const sut = testContext
|
const sut = testContext
|
||||||
.withInitialOs(OperatingSystem.Windows)
|
.withInitialOs(OperatingSystem.Windows)
|
||||||
.construct();
|
.construct();
|
||||||
|
const oldState = sut.state;
|
||||||
sut.changeContext(nextOs);
|
sut.changeContext(nextOs);
|
||||||
// assert
|
// assert
|
||||||
expect(testContext.firedEvents.length).to.equal(1);
|
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].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', () => {
|
it('is not fired when initial os is changed to same one', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -148,7 +150,7 @@ describe('ApplicationContext', () => {
|
|||||||
.withInitialOs(os)
|
.withInitialOs(os)
|
||||||
.construct();
|
.construct();
|
||||||
// assert
|
// assert
|
||||||
const actual = sut.collection;
|
const actual = sut.state.collection;
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actual).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -174,7 +176,7 @@ describe('ApplicationContext', () => {
|
|||||||
.withInitialOs(expected)
|
.withInitialOs(expected)
|
||||||
.construct();
|
.construct();
|
||||||
// assert
|
// assert
|
||||||
const actual = sut.currentOs;
|
const actual = sut.state.os;
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actual).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
describe('throws when OS is invalid', () => {
|
describe('throws when OS is invalid', () => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe('ApplicationContextProvider', () => {
|
|||||||
// act
|
// act
|
||||||
const context = buildContext(parserMock);
|
const context = buildContext(parserMock);
|
||||||
// assert
|
// assert
|
||||||
// TODO: expect(expected).to.equal(context.app);
|
expect(expected).to.equal(context.app);
|
||||||
});
|
});
|
||||||
describe('sets initial OS as expected', () => {
|
describe('sets initial OS as expected', () => {
|
||||||
it('returns currentOs if it is supported', () => {
|
it('returns currentOs if it is supported', () => {
|
||||||
@@ -28,7 +28,8 @@ describe('ApplicationContextProvider', () => {
|
|||||||
// act
|
// act
|
||||||
const context = buildContext(parser, environment);
|
const context = buildContext(parser, environment);
|
||||||
// assert
|
// 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', () => {
|
it('fallbacks to other os if OS in environment is not supported', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -39,11 +40,25 @@ describe('ApplicationContextProvider', () => {
|
|||||||
// act
|
// act
|
||||||
const context = buildContext(parser, environment);
|
const context = buildContext(parser, environment);
|
||||||
// assert
|
// assert
|
||||||
const actual = context.currentOs;
|
const actual = context.state.os;
|
||||||
expect(expected).to.equal(actual);
|
expect(expected).to.equal(actual);
|
||||||
});
|
});
|
||||||
it('fallbacks to most supported os if current os is not supported', () => {
|
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]}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { expect } from 'chai';
|
|||||||
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
import { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||||
import { ApplicationCode } from '@/application/Context/State/Code/ApplicationCode';
|
import { ApplicationCode } from '@/application/Context/State/Code/ApplicationCode';
|
||||||
import { CategoryCollectionState } from '@/application/Context/State/CategoryCollectionState';
|
import { CategoryCollectionState } from '@/application/Context/State/CategoryCollectionState';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ScriptStub } from '../../../stubs/ScriptStub';
|
import { ScriptStub } from '../../../stubs/ScriptStub';
|
||||||
import { CategoryStub } from '../../../stubs/CategoryStub';
|
import { CategoryStub } from '../../../stubs/CategoryStub';
|
||||||
@@ -21,7 +22,8 @@ describe('CategoryCollectionState', () => {
|
|||||||
});
|
});
|
||||||
it('reacts to selection changes as expected', () => {
|
it('reacts to selection changes as expected', () => {
|
||||||
// arrange
|
// 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 selectionStub = new UserSelection(collection, []);
|
||||||
const expectedCodeGenerator = new ApplicationCode(selectionStub, collection.scripting);
|
const expectedCodeGenerator = new ApplicationCode(selectionStub, collection.scripting);
|
||||||
selectionStub.selectAll();
|
selectionStub.selectAll();
|
||||||
@@ -34,6 +36,19 @@ describe('CategoryCollectionState', () => {
|
|||||||
expect(actualCode).to.equal(expectedCode);
|
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', () => {
|
describe('selection', () => {
|
||||||
it('initialized with no selection', () => {
|
it('initialized with no selection', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -70,7 +85,8 @@ describe('CategoryCollectionState', () => {
|
|||||||
it('can match a script from current collection', () => {
|
it('can match a script from current collection', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const scriptNameFilter = 'scriptName';
|
const scriptNameFilter = 'scriptName';
|
||||||
const expectedScript = new ScriptStub('scriptId').withName(scriptNameFilter);
|
const expectedScript = new ScriptStub('scriptId')
|
||||||
|
.withName(scriptNameFilter);
|
||||||
const collection = new CategoryCollectionStub()
|
const collection = new CategoryCollectionStub()
|
||||||
.withAction(new CategoryStub(0).withScript(expectedScript));
|
.withAction(new CategoryStub(0).withScript(expectedScript));
|
||||||
const sut = new CategoryCollectionState(collection);
|
const sut = new CategoryCollectionState(collection);
|
||||||
@@ -3,10 +3,23 @@ import { expect } from 'chai';
|
|||||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||||
|
|
||||||
describe('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', () => {
|
describe('appendLine', () => {
|
||||||
it('when empty appends empty line', () => {
|
it('when empty appends empty line', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new CodeBuilder();
|
const sut = new CodeBuilderConcrete();
|
||||||
// act
|
// act
|
||||||
sut.appendLine().appendLine().appendLine();
|
sut.appendLine().appendLine().appendLine();
|
||||||
// assert
|
// assert
|
||||||
@@ -14,7 +27,7 @@ describe('CodeBuilder', () => {
|
|||||||
});
|
});
|
||||||
it('when not empty append string in new line', () => {
|
it('when not empty append string in new line', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new CodeBuilder();
|
const sut = new CodeBuilderConcrete();
|
||||||
const expected = 'str';
|
const expected = 'str';
|
||||||
// act
|
// act
|
||||||
sut.appendLine()
|
sut.appendLine()
|
||||||
@@ -27,7 +40,7 @@ describe('CodeBuilder', () => {
|
|||||||
});
|
});
|
||||||
it('appendFunction', () => {
|
it('appendFunction', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new CodeBuilder();
|
const sut = new CodeBuilderConcrete();
|
||||||
const functionName = 'function';
|
const functionName = 'function';
|
||||||
const code = 'code';
|
const code = 'code';
|
||||||
// act
|
// act
|
||||||
@@ -39,11 +52,13 @@ describe('CodeBuilder', () => {
|
|||||||
});
|
});
|
||||||
it('appendTrailingHyphensCommentLine', () => {
|
it('appendTrailingHyphensCommentLine', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new CodeBuilder();
|
const commentDelimiter = '//';
|
||||||
const totalHypens = 5;
|
const sut = new CodeBuilderConcrete()
|
||||||
const expected = `:: ${'-'.repeat(totalHypens)}`;
|
.withCommentDelimiter(commentDelimiter);
|
||||||
|
const totalHyphens = 5;
|
||||||
|
const expected = `${commentDelimiter} ${'-'.repeat(totalHyphens)}`;
|
||||||
// act
|
// act
|
||||||
sut.appendTrailingHyphensCommentLine(totalHypens);
|
sut.appendTrailingHyphensCommentLine(totalHyphens);
|
||||||
// assert
|
// assert
|
||||||
const result = sut.toString();
|
const result = sut.toString();
|
||||||
const lines = getLines(result);
|
const lines = getLines(result);
|
||||||
@@ -51,38 +66,45 @@ describe('CodeBuilder', () => {
|
|||||||
});
|
});
|
||||||
it('appendCommentLine', () => {
|
it('appendCommentLine', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new CodeBuilder();
|
const commentDelimiter = '//';
|
||||||
|
const sut = new CodeBuilderConcrete()
|
||||||
|
.withCommentDelimiter(commentDelimiter);
|
||||||
const comment = 'comment';
|
const comment = 'comment';
|
||||||
const expected = ':: comment';
|
const expected = `${commentDelimiter} comment`;
|
||||||
// act
|
// act
|
||||||
sut.appendCommentLine(comment);
|
const result = sut
|
||||||
|
.appendCommentLine(comment)
|
||||||
|
.toString();
|
||||||
// assert
|
// assert
|
||||||
const result = sut.toString();
|
|
||||||
const lines = getLines(result);
|
const lines = getLines(result);
|
||||||
expect(lines[0]).to.equal(expected);
|
expect(lines[0]).to.equal(expected);
|
||||||
});
|
});
|
||||||
it('appendCommentLineWithHyphensAround', () => {
|
it('appendCommentLineWithHyphensAround', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new CodeBuilder();
|
const commentDelimiter = '//';
|
||||||
|
const sut = new CodeBuilderConcrete()
|
||||||
|
.withCommentDelimiter(commentDelimiter);
|
||||||
const sectionName = 'section';
|
const sectionName = 'section';
|
||||||
const totalHypens = sectionName.length + 3 * 2;
|
const totalHyphens = sectionName.length + 3 * 2;
|
||||||
const expected = ':: ---section---';
|
const expected = `${commentDelimiter} ---section---`;
|
||||||
sut.appendCommentLineWithHyphensAround(sectionName, totalHypens);
|
// act
|
||||||
|
const result = sut
|
||||||
|
.appendCommentLineWithHyphensAround(sectionName, totalHyphens)
|
||||||
|
.toString();
|
||||||
// assert
|
// assert
|
||||||
const result = sut.toString();
|
|
||||||
const lines = getLines(result);
|
const lines = getLines(result);
|
||||||
expect(lines[1]).to.equal(expected);
|
expect(lines[1]).to.equal(expected);
|
||||||
});
|
});
|
||||||
describe('currentLine', () => {
|
describe('currentLine', () => {
|
||||||
it('no lines returns zero', () => {
|
it('no lines returns zero', () => {
|
||||||
// arrange & act
|
// arrange & act
|
||||||
const sut = new CodeBuilder();
|
const sut = new CodeBuilderConcrete();
|
||||||
// assert
|
// assert
|
||||||
expect(sut.currentLine).to.equal(0);
|
expect(sut.currentLine).to.equal(0);
|
||||||
});
|
});
|
||||||
it('single line returns one', () => {
|
it('single line returns one', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new CodeBuilder();
|
const sut = new CodeBuilderConcrete();
|
||||||
// act
|
// act
|
||||||
sut.appendLine();
|
sut.appendLine();
|
||||||
// assert
|
// assert
|
||||||
@@ -90,15 +112,17 @@ describe('CodeBuilder', () => {
|
|||||||
});
|
});
|
||||||
it('multiple lines returns as expected', () => {
|
it('multiple lines returns as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new CodeBuilder();
|
const sut = new CodeBuilderConcrete();
|
||||||
// act
|
// act
|
||||||
sut.appendLine('1').appendCommentLine('2').appendLine();
|
sut.appendLine('1')
|
||||||
|
.appendCommentLine('2')
|
||||||
|
.appendLine();
|
||||||
// assert
|
// assert
|
||||||
expect(sut.currentLine).to.equal(3);
|
expect(sut.currentLine).to.equal(3);
|
||||||
});
|
});
|
||||||
it('multiple lines in code', () => {
|
it('multiple lines in code', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new CodeBuilder();
|
const sut = new CodeBuilderConcrete();
|
||||||
// act
|
// act
|
||||||
sut.appendLine('hello\ncode-here\nwith-3-lines');
|
sut.appendLine('hello\ncode-here\nwith-3-lines');
|
||||||
// assert
|
// assert
|
||||||
|
|||||||
@@ -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]}"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,8 @@ import 'mocha';
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { UserScriptGenerator } from '@/application/Context/State/Code/Generation/UserScriptGenerator';
|
import { UserScriptGenerator } from '@/application/Context/State/Code/Generation/UserScriptGenerator';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
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 { ScriptStub } from '../../../../../stubs/ScriptStub';
|
||||||
import { ScriptingDefinitionStub } from '../../../../../stubs/ScriptingDefinitionStub';
|
import { ScriptingDefinitionStub } from '../../../../../stubs/ScriptingDefinitionStub';
|
||||||
|
|
||||||
@@ -28,14 +29,15 @@ describe('UserScriptGenerator', () => {
|
|||||||
});
|
});
|
||||||
it('is not prepended if empty', () => {
|
it('is not prepended if empty', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new UserScriptGenerator();
|
const codeBuilderStub = new CodeBuilderStub();
|
||||||
|
const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub));
|
||||||
const script = new ScriptStub('id')
|
const script = new ScriptStub('id')
|
||||||
.withCode('code\nmulti-lined')
|
.withCode('code\nmulti-lined')
|
||||||
.toSelectedScript();
|
.toSelectedScript();
|
||||||
const definition = new ScriptingDefinitionStub()
|
const definition = new ScriptingDefinitionStub()
|
||||||
.withStartCode(undefined)
|
.withStartCode(undefined)
|
||||||
.withEndCode(undefined);
|
.withEndCode(undefined);
|
||||||
const expectedStart = new CodeBuilder()
|
const expectedStart = codeBuilderStub
|
||||||
.appendFunction(script.script.name, script.script.code.execute)
|
.appendFunction(script.script.name, script.script.code.execute)
|
||||||
.toString();
|
.toString();
|
||||||
// act
|
// act
|
||||||
@@ -64,15 +66,16 @@ describe('UserScriptGenerator', () => {
|
|||||||
});
|
});
|
||||||
it('is not appended if empty', () => {
|
it('is not appended if empty', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const sut = new UserScriptGenerator();
|
const codeBuilderStub = new CodeBuilderStub();
|
||||||
|
const sut = new UserScriptGenerator(mockCodeBuilderFactory(codeBuilderStub));
|
||||||
const script = new ScriptStub('id')
|
const script = new ScriptStub('id')
|
||||||
.withCode('code\nmulti-lined')
|
.withCode('code\nmulti-lined')
|
||||||
.toSelectedScript();
|
.toSelectedScript();
|
||||||
const definition = new ScriptingDefinitionStub()
|
const expectedEnd = codeBuilderStub
|
||||||
.withEndCode(undefined);
|
|
||||||
const expectedEnd = new CodeBuilder()
|
|
||||||
.appendFunction(script.script.name, script.script.code.execute)
|
.appendFunction(script.script.name, script.script.code.execute)
|
||||||
.toString();
|
.toString();
|
||||||
|
const definition = new ScriptingDefinitionStub()
|
||||||
|
.withEndCode(undefined);
|
||||||
// act
|
// act
|
||||||
const code = sut.buildCode([script], definition);
|
const code = sut.buildCode([script], definition);
|
||||||
// assert
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { expect } from 'chai';
|
|||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser';
|
import { CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser';
|
||||||
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
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 { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { getEnumValues } from '@/application/Common/Enum';
|
||||||
import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub';
|
||||||
import { getProcessEnvironmentStub } from '../../stubs/ProcessEnvironmentStub';
|
import { getProcessEnvironmentStub } from '../../stubs/ProcessEnvironmentStub';
|
||||||
import { CollectionDataStub } from '../../stubs/CollectionDataStub';
|
import { CollectionDataStub } from '../../stubs/CollectionDataStub';
|
||||||
@@ -24,15 +26,18 @@ describe('ApplicationParser', () => {
|
|||||||
it('returns result from the parser', () => {
|
it('returns result from the parser', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const os = OperatingSystem.macOS;
|
const os = OperatingSystem.macOS;
|
||||||
|
const data = new CollectionDataStub();
|
||||||
const expected = new CategoryCollectionStub()
|
const expected = new CategoryCollectionStub()
|
||||||
.withOs(os);
|
.withOs(os);
|
||||||
const parser = new CategoryCollectionParserSpy()
|
const parser = new CategoryCollectionParserSpy()
|
||||||
.setResult(expected)
|
.setUpReturnValue(data, expected)
|
||||||
.mockParser();
|
.mockParser();
|
||||||
|
const env = getProcessEnvironmentStub();
|
||||||
|
const collections = [ data ];
|
||||||
// act
|
// act
|
||||||
const context = parseApplication(parser);
|
const app = parseApplication(parser, env, collections);
|
||||||
// assert
|
// assert
|
||||||
const actual = context.getCollection(os);
|
const actual = app.getCollection(os);
|
||||||
expect(expected).to.equal(actual);
|
expect(expected).to.equal(actual);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -44,10 +49,10 @@ describe('ApplicationParser', () => {
|
|||||||
const parserSpy = new CategoryCollectionParserSpy();
|
const parserSpy = new CategoryCollectionParserSpy();
|
||||||
const parserMock = parserSpy.mockParser();
|
const parserMock = parserSpy.mockParser();
|
||||||
// act
|
// act
|
||||||
const context = parseApplication(parserMock, env);
|
const app = parseApplication(parserMock, env);
|
||||||
// assert
|
// assert
|
||||||
expect(expected).to.deep.equal(context.info);
|
expect(expected).to.deep.equal(app.info);
|
||||||
expect(expected).to.deep.equal(parserSpy.lastArguments.info);
|
expect(parserSpy.arguments.map((arg) => arg.info).every((info) => info === expected));
|
||||||
});
|
});
|
||||||
it('defaults to process.env', () => {
|
it('defaults to process.env', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -56,54 +61,110 @@ describe('ApplicationParser', () => {
|
|||||||
const parserSpy = new CategoryCollectionParserSpy();
|
const parserSpy = new CategoryCollectionParserSpy();
|
||||||
const parserMock = parserSpy.mockParser();
|
const parserMock = parserSpy.mockParser();
|
||||||
// act
|
// act
|
||||||
const context = parseApplication(parserMock);
|
const app = parseApplication(parserMock);
|
||||||
// assert
|
// assert
|
||||||
expect(expected).to.deep.equal(context.info);
|
expect(expected).to.deep.equal(app.info);
|
||||||
expect(expected).to.deep.equal(parserSpy.lastArguments.info);
|
expect(parserSpy.arguments.map((arg) => arg.info).every((info) => info === expected));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('collectionData', () => {
|
describe('collectionsData', () => {
|
||||||
it('parsed with expected data', () => {
|
describe('set as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = new CollectionDataStub();
|
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
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
it(testCase.name, () => {
|
||||||
const env = getProcessEnvironmentStub();
|
const env = getProcessEnvironmentStub();
|
||||||
const parserSpy = new CategoryCollectionParserSpy();
|
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();
|
const parserMock = parserSpy.mockParser();
|
||||||
// act
|
// act
|
||||||
parseApplication(parserMock, env, expected);
|
const app = parseApplication(parserMock, env, testCase.input);
|
||||||
// assert
|
// assert
|
||||||
expect(expected).to.equal(parserSpy.lastArguments.file);
|
expect(app.collections).to.deep.equal(testCase.output);
|
||||||
});
|
});
|
||||||
it('defaults to windows data', () => {
|
}
|
||||||
|
});
|
||||||
|
it('defaults to expected data', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = WindowsData;
|
const expected = [ WindowsData, MacOsData ];
|
||||||
const parserSpy = new CategoryCollectionParserSpy();
|
const parserSpy = new CategoryCollectionParserSpy();
|
||||||
const parserMock = parserSpy.mockParser();
|
const parserMock = parserSpy.mockParser();
|
||||||
// act
|
// act
|
||||||
parseApplication(parserMock);
|
parseApplication(parserMock);
|
||||||
// assert
|
// 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 {
|
class CategoryCollectionParserSpy {
|
||||||
public lastArguments: {
|
public arguments = new Array<{
|
||||||
file: CollectionData;
|
data: CollectionData,
|
||||||
info: ProjectInformation;
|
info: ProjectInformation,
|
||||||
} = { file: undefined, info: undefined };
|
}>();
|
||||||
private result: ICategoryCollection = new CategoryCollectionStub();
|
|
||||||
|
|
||||||
public setResult(collection: ICategoryCollection): CategoryCollectionParserSpy {
|
private returnValues = new Map<CollectionData, ICategoryCollection>();
|
||||||
this.result = collection;
|
|
||||||
|
public setUpReturnValue(data: CollectionData, collection: ICategoryCollection): CategoryCollectionParserSpy {
|
||||||
|
this.returnValues.set(data, collection);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
public mockParser(): CategoryCollectionParserType {
|
public mockParser(): CategoryCollectionParserType {
|
||||||
return (file: CollectionData, info: IProjectInformation) => {
|
return (data: CollectionData, info: IProjectInformation) => {
|
||||||
this.lastArguments.file = file;
|
this.arguments.push({ data, info });
|
||||||
this.lastArguments.info = info;
|
if (this.returnValues.has(data)) {
|
||||||
return this.result;
|
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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
|||||||
import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinitionParser';
|
import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinitionParser';
|
||||||
import { mockEnumParser } from '../../stubs/EnumParserStub';
|
import { mockEnumParser } from '../../stubs/EnumParserStub';
|
||||||
import { ProjectInformationStub } from '../../stubs/ProjectInformationStub';
|
import { ProjectInformationStub } from '../../stubs/ProjectInformationStub';
|
||||||
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
|
|
||||||
import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub';
|
import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub';
|
||||||
|
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
|
||||||
|
|
||||||
describe('CategoryCollectionParser', () => {
|
describe('CategoryCollectionParser', () => {
|
||||||
describe('parseCategoryCollection', () => {
|
describe('parseCategoryCollection', () => {
|
||||||
@@ -48,8 +48,8 @@ describe('CategoryCollectionParser', () => {
|
|||||||
it('parses actions', () => {
|
it('parses actions', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const actions = [ getCategoryStub('test1'), getCategoryStub('test2') ];
|
const actions = [ getCategoryStub('test1'), getCategoryStub('test2') ];
|
||||||
const compiler = new ScriptCompilerStub();
|
const context = new CategoryCollectionParseContextStub();
|
||||||
const expected = [ parseCategory(actions[0], compiler), parseCategory(actions[1], compiler) ];
|
const expected = [ parseCategory(actions[0], context), parseCategory(actions[1], context) ];
|
||||||
const collection = new CollectionDataStub()
|
const collection = new CollectionDataStub()
|
||||||
.withActions(actions);
|
.withActions(actions);
|
||||||
const info = new ProjectInformationStub();
|
const info = new ProjectInformationStub();
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'mocha';
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { parseCategory } from '@/application/Parser/CategoryParser';
|
import { parseCategory } from '@/application/Parser/CategoryParser';
|
||||||
import { CategoryData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
|
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 { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||||
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
|
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
|
||||||
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
|
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
|
||||||
|
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
|
||||||
|
import { LanguageSyntaxStub } from '../../stubs/LanguageSyntaxStub';
|
||||||
|
|
||||||
describe('CategoryParser', () => {
|
describe('CategoryParser', () => {
|
||||||
describe('parseCategory', () => {
|
describe('parseCategory', () => {
|
||||||
@@ -14,9 +16,9 @@ describe('CategoryParser', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const expectedMessage = 'category is null or undefined';
|
const expectedMessage = 'category is null or undefined';
|
||||||
const category = undefined;
|
const category = undefined;
|
||||||
const compiler = new ScriptCompilerStub();
|
const context = new CategoryCollectionParseContextStub();
|
||||||
// act
|
// act
|
||||||
const act = () => parseCategory(category, compiler);
|
const act = () => parseCategory(category, context);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedMessage);
|
expect(act).to.throw(expectedMessage);
|
||||||
});
|
});
|
||||||
@@ -28,9 +30,9 @@ describe('CategoryParser', () => {
|
|||||||
category: categoryName,
|
category: categoryName,
|
||||||
children: [],
|
children: [],
|
||||||
};
|
};
|
||||||
const compiler = new ScriptCompilerStub();
|
const context = new CategoryCollectionParseContextStub();
|
||||||
// act
|
// act
|
||||||
const act = () => parseCategory(category, compiler);
|
const act = () => parseCategory(category, context);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedMessage);
|
expect(act).to.throw(expectedMessage);
|
||||||
});
|
});
|
||||||
@@ -42,9 +44,9 @@ describe('CategoryParser', () => {
|
|||||||
category: categoryName,
|
category: categoryName,
|
||||||
children: undefined,
|
children: undefined,
|
||||||
};
|
};
|
||||||
const compiler = new ScriptCompilerStub();
|
const context = new CategoryCollectionParseContextStub();
|
||||||
// act
|
// act
|
||||||
const act = () => parseCategory(category, compiler);
|
const act = () => parseCategory(category, context);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedMessage);
|
expect(act).to.throw(expectedMessage);
|
||||||
});
|
});
|
||||||
@@ -57,21 +59,21 @@ describe('CategoryParser', () => {
|
|||||||
category: invalidName,
|
category: invalidName,
|
||||||
children: getTestChildren(),
|
children: getTestChildren(),
|
||||||
};
|
};
|
||||||
const compiler = new ScriptCompilerStub();
|
const context = new CategoryCollectionParseContextStub();
|
||||||
// act
|
// act
|
||||||
const act = () => parseCategory(category, compiler);
|
const act = () => parseCategory(category, context);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedMessage);
|
expect(act).to.throw(expectedMessage);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('throws when compiler is undefined', () => {
|
it('throws when context is undefined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'undefined compiler';
|
const expectedError = 'undefined context';
|
||||||
const compiler = undefined;
|
const context = undefined;
|
||||||
const category = getValidCategory();
|
const category = getValidCategory();
|
||||||
// act
|
// act
|
||||||
const act = () => parseCategory(category, compiler);
|
const act = () => parseCategory(category, context);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
@@ -79,14 +81,14 @@ describe('CategoryParser', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const url = 'https://privacy.sexy';
|
const url = 'https://privacy.sexy';
|
||||||
const expected = parseDocUrls({ docs: url });
|
const expected = parseDocUrls({ docs: url });
|
||||||
const compiler = new ScriptCompilerStub();
|
|
||||||
const category: CategoryData = {
|
const category: CategoryData = {
|
||||||
category: 'category name',
|
category: 'category name',
|
||||||
children: getTestChildren(),
|
children: getTestChildren(),
|
||||||
docs: url,
|
docs: url,
|
||||||
};
|
};
|
||||||
|
const context = new CategoryCollectionParseContextStub();
|
||||||
// act
|
// act
|
||||||
const actual = parseCategory(category, compiler).documentationUrls;
|
const actual = parseCategory(category, context).documentationUrls;
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actual).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
@@ -94,14 +96,14 @@ describe('CategoryParser', () => {
|
|||||||
it('single script with code', () => {
|
it('single script with code', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const script = ScriptDataStub.createWithCode();
|
const script = ScriptDataStub.createWithCode();
|
||||||
const compiler = new ScriptCompilerStub();
|
const context = new CategoryCollectionParseContextStub();
|
||||||
const expected = [ parseScript(script, compiler) ];
|
const expected = [ parseScript(script, context) ];
|
||||||
const category: CategoryData = {
|
const category: CategoryData = {
|
||||||
category: 'category name',
|
category: 'category name',
|
||||||
children: [ script ],
|
children: [ script ],
|
||||||
};
|
};
|
||||||
// act
|
// act
|
||||||
const actual = parseCategory(category, compiler).scripts;
|
const actual = parseCategory(category, context).scripts;
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actual).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
@@ -110,13 +112,15 @@ describe('CategoryParser', () => {
|
|||||||
const script = ScriptDataStub.createWithCall();
|
const script = ScriptDataStub.createWithCall();
|
||||||
const compiler = new ScriptCompilerStub()
|
const compiler = new ScriptCompilerStub()
|
||||||
.withCompileAbility(script);
|
.withCompileAbility(script);
|
||||||
const expected = [ parseScript(script, compiler) ];
|
const context = new CategoryCollectionParseContextStub()
|
||||||
|
.withCompiler(compiler);
|
||||||
|
const expected = [ parseScript(script, context) ];
|
||||||
const category: CategoryData = {
|
const category: CategoryData = {
|
||||||
category: 'category name',
|
category: 'category name',
|
||||||
children: [ script ],
|
children: [ script ],
|
||||||
};
|
};
|
||||||
// act
|
// act
|
||||||
const actual = parseCategory(category, compiler).scripts;
|
const actual = parseCategory(category, context).scripts;
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actual).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
@@ -124,18 +128,44 @@ describe('CategoryParser', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const callableScript = ScriptDataStub.createWithCall();
|
const callableScript = ScriptDataStub.createWithCall();
|
||||||
const scripts = [ callableScript, ScriptDataStub.createWithCode() ];
|
const scripts = [ callableScript, ScriptDataStub.createWithCode() ];
|
||||||
const compiler = new ScriptCompilerStub()
|
|
||||||
.withCompileAbility(callableScript);
|
|
||||||
const expected = scripts.map((script) => parseScript(script, compiler));
|
|
||||||
const category: CategoryData = {
|
const category: CategoryData = {
|
||||||
category: 'category name',
|
category: 'category name',
|
||||||
children: scripts,
|
children: scripts,
|
||||||
};
|
};
|
||||||
|
const compiler = new ScriptCompilerStub()
|
||||||
|
.withCompileAbility(callableScript);
|
||||||
|
const context = new CategoryCollectionParseContextStub()
|
||||||
|
.withCompiler(compiler);
|
||||||
|
const expected = scripts.map((script) => parseScript(script, context));
|
||||||
// act
|
// act
|
||||||
const actual = parseCategory(category, compiler).scripts;
|
const actual = parseCategory(category, context).scripts;
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.deep.equal(expected);
|
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', () => {
|
it('returns expected subcategories', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -147,9 +177,9 @@ describe('CategoryParser', () => {
|
|||||||
category: 'category name',
|
category: 'category name',
|
||||||
children: expected,
|
children: expected,
|
||||||
};
|
};
|
||||||
const compiler = new ScriptCompilerStub();
|
const context = new CategoryCollectionParseContextStub();
|
||||||
// act
|
// act
|
||||||
const actual = parseCategory(category, compiler).subCategories;
|
const actual = parseCategory(category, context).subCategories;
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.have.lengthOf(1);
|
expect(actual).to.have.lengthOf(1);
|
||||||
expect(actual[0].name).to.equal(expected[0].category);
|
expect(actual[0].name).to.equal(expected[0].category);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { generateIlCode } from '@/application/Parser/Compiler/ILCode';
|
import { generateIlCode } from '@/application/Parser/Script/Compiler/ILCode';
|
||||||
|
|
||||||
describe('ILCode', () => {
|
describe('ILCode', () => {
|
||||||
describe('getUniqueParameterNames', () => {
|
describe('getUniqueParameterNames', () => {
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { parseScript } from '@/application/Parser/ScriptParser';
|
import { parseScript } from '@/application/Parser/Script/ScriptParser';
|
||||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
||||||
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
|
import { ScriptCompilerStub } from '../../../stubs/ScriptCompilerStub';
|
||||||
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
|
import { ScriptDataStub } from '../../../stubs/ScriptDataStub';
|
||||||
import { mockEnumParser } from '../../stubs/EnumParserStub';
|
import { mockEnumParser } from '../../../stubs/EnumParserStub';
|
||||||
|
import { ScriptCodeStub } from '../../../stubs/ScriptCodeStub';
|
||||||
|
import { CategoryCollectionParseContextStub } from '../../../stubs/CategoryCollectionParseContextStub';
|
||||||
|
import { LanguageSyntaxStub } from '../../../stubs/LanguageSyntaxStub';
|
||||||
|
|
||||||
describe('ScriptParser', () => {
|
describe('ScriptParser', () => {
|
||||||
describe('parseScript', () => {
|
describe('parseScript', () => {
|
||||||
@@ -15,9 +18,9 @@ describe('ScriptParser', () => {
|
|||||||
const expected = 'test-expected-name';
|
const expected = 'test-expected-name';
|
||||||
const script = ScriptDataStub.createWithCode()
|
const script = ScriptDataStub.createWithCode()
|
||||||
.withName(expected);
|
.withName(expected);
|
||||||
const compiler = new ScriptCompilerStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
// act
|
// act
|
||||||
const actual = parseScript(script, compiler);
|
const actual = parseScript(script, parseContext);
|
||||||
// assert
|
// assert
|
||||||
expect(actual.name).to.equal(expected);
|
expect(actual.name).to.equal(expected);
|
||||||
});
|
});
|
||||||
@@ -26,10 +29,10 @@ describe('ScriptParser', () => {
|
|||||||
const docs = [ 'https://expected-doc1.com', 'https://expected-doc2.com' ];
|
const docs = [ 'https://expected-doc1.com', 'https://expected-doc2.com' ];
|
||||||
const script = ScriptDataStub.createWithCode()
|
const script = ScriptDataStub.createWithCode()
|
||||||
.withDocs(docs);
|
.withDocs(docs);
|
||||||
const compiler = new ScriptCompilerStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
const expected = parseDocUrls(script);
|
const expected = parseDocUrls(script);
|
||||||
// act
|
// act
|
||||||
const actual = parseScript(script, compiler);
|
const actual = parseScript(script, parseContext);
|
||||||
// assert
|
// assert
|
||||||
expect(actual.documentationUrls).to.deep.equal(expected);
|
expect(actual.documentationUrls).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
@@ -37,44 +40,44 @@ describe('ScriptParser', () => {
|
|||||||
it('throws when script is undefined', () => {
|
it('throws when script is undefined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'undefined script';
|
const expectedError = 'undefined script';
|
||||||
const compiler = new ScriptCompilerStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
const script = undefined;
|
const script = undefined;
|
||||||
// act
|
// act
|
||||||
const act = () => parseScript(script, compiler);
|
const act = () => parseScript(script, parseContext);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
it('throws when both function call and code are defined', () => {
|
it('throws when both function call and code are defined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'cannot define both "call" and "code"';
|
const expectedError = 'cannot define both "call" and "code"';
|
||||||
const compiler = new ScriptCompilerStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
const script = ScriptDataStub
|
const script = ScriptDataStub
|
||||||
.createWithCall()
|
.createWithCall()
|
||||||
.withCode('code');
|
.withCode('code');
|
||||||
// act
|
// act
|
||||||
const act = () => parseScript(script, compiler);
|
const act = () => parseScript(script, parseContext);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
it('throws when both function call and revertCode are defined', () => {
|
it('throws when both function call and revertCode are defined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'cannot define "revertCode" if "call" is defined';
|
const expectedError = 'cannot define "revertCode" if "call" is defined';
|
||||||
const compiler = new ScriptCompilerStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
const script = ScriptDataStub
|
const script = ScriptDataStub
|
||||||
.createWithCall()
|
.createWithCall()
|
||||||
.withRevertCode('revert-code');
|
.withRevertCode('revert-code');
|
||||||
// act
|
// act
|
||||||
const act = () => parseScript(script, compiler);
|
const act = () => parseScript(script, parseContext);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
it('throws when neither call or revertCode are defined', () => {
|
it('throws when neither call or revertCode are defined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'must define either "call" or "code"';
|
const expectedError = 'must define either "call" or "code"';
|
||||||
const compiler = new ScriptCompilerStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
const script = ScriptDataStub.createWithoutCallOrCodes();
|
const script = ScriptDataStub.createWithoutCallOrCodes();
|
||||||
// act
|
// act
|
||||||
const act = () => parseScript(script, compiler);
|
const act = () => parseScript(script, parseContext);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
@@ -84,11 +87,11 @@ describe('ScriptParser', () => {
|
|||||||
const undefinedLevels: string[] = [ '', undefined ];
|
const undefinedLevels: string[] = [ '', undefined ];
|
||||||
undefinedLevels.forEach((undefinedLevel) => {
|
undefinedLevels.forEach((undefinedLevel) => {
|
||||||
// arrange
|
// arrange
|
||||||
const compiler = new ScriptCompilerStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
const script = ScriptDataStub.createWithCode()
|
const script = ScriptDataStub.createWithCode()
|
||||||
.withRecommend(undefinedLevel);
|
.withRecommend(undefinedLevel);
|
||||||
// act
|
// act
|
||||||
const actual = parseScript(script, compiler);
|
const actual = parseScript(script, parseContext);
|
||||||
// assert
|
// assert
|
||||||
expect(actual.level).to.equal(undefined);
|
expect(actual.level).to.equal(undefined);
|
||||||
});
|
});
|
||||||
@@ -100,10 +103,10 @@ describe('ScriptParser', () => {
|
|||||||
const levelText = 'standard';
|
const levelText = 'standard';
|
||||||
const script = ScriptDataStub.createWithCode()
|
const script = ScriptDataStub.createWithCode()
|
||||||
.withRecommend(levelText);
|
.withRecommend(levelText);
|
||||||
const compiler = new ScriptCompilerStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
const parserMock = mockEnumParser(expectedName, levelText, expectedLevel);
|
const parserMock = mockEnumParser(expectedName, levelText, expectedLevel);
|
||||||
// act
|
// act
|
||||||
const actual = parseScript(script, compiler, parserMock);
|
const actual = parseScript(script, parseContext, parserMock);
|
||||||
// assert
|
// assert
|
||||||
expect(actual.level).to.equal(expectedLevel);
|
expect(actual.level).to.equal(expectedLevel);
|
||||||
});
|
});
|
||||||
@@ -115,9 +118,9 @@ describe('ScriptParser', () => {
|
|||||||
const script = ScriptDataStub
|
const script = ScriptDataStub
|
||||||
.createWithCode()
|
.createWithCode()
|
||||||
.withCode(expected);
|
.withCode(expected);
|
||||||
const compiler = new ScriptCompilerStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
// act
|
// act
|
||||||
const parsed = parseScript(script, compiler);
|
const parsed = parseScript(script, parseContext);
|
||||||
// assert
|
// assert
|
||||||
const actual = parsed.code.execute;
|
const actual = parsed.code.execute;
|
||||||
expect(actual).to.equal(expected);
|
expect(actual).to.equal(expected);
|
||||||
@@ -128,36 +131,55 @@ describe('ScriptParser', () => {
|
|||||||
const script = ScriptDataStub
|
const script = ScriptDataStub
|
||||||
.createWithCode()
|
.createWithCode()
|
||||||
.withRevertCode(expected);
|
.withRevertCode(expected);
|
||||||
const compiler = new ScriptCompilerStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
// act
|
// act
|
||||||
const parsed = parseScript(script, compiler);
|
const parsed = parseScript(script, parseContext);
|
||||||
// assert
|
// assert
|
||||||
const actual = parsed.code.revert;
|
const actual = parsed.code.revert;
|
||||||
expect(actual).to.equal(expected);
|
expect(actual).to.equal(expected);
|
||||||
});
|
});
|
||||||
describe('compiler', () => {
|
describe('compiler', () => {
|
||||||
it('throws when compiler is not defined', () => {
|
it('throws when context is not defined', () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const expectedMessage = 'undefined context';
|
||||||
const script = ScriptDataStub.createWithCode();
|
const script = ScriptDataStub.createWithCode();
|
||||||
const compiler = undefined;
|
const context: ICategoryCollectionParseContext = undefined;
|
||||||
// act
|
// act
|
||||||
const act = () => parseScript(script, compiler);
|
const act = () => parseScript(script, context);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw('undefined compiler');
|
expect(act).to.throw(expectedMessage);
|
||||||
});
|
});
|
||||||
it('gets code from compiler', () => {
|
it('gets code from compiler', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = new ScriptCode('test-script', 'code', 'revert-code');
|
const expected = new ScriptCodeStub();
|
||||||
const script = ScriptDataStub.createWithCode();
|
const script = ScriptDataStub.createWithCode();
|
||||||
const compiler = new ScriptCompilerStub()
|
const compiler = new ScriptCompilerStub()
|
||||||
.withCompileAbility(script, expected);
|
.withCompileAbility(script, expected);
|
||||||
|
const parseContext = new CategoryCollectionParseContextStub()
|
||||||
|
.withCompiler(compiler);
|
||||||
// act
|
// act
|
||||||
const parsed = parseScript(script, compiler);
|
const parsed = parseScript(script, parseContext);
|
||||||
// assert
|
// assert
|
||||||
const actual = parsed.code;
|
const actual = parsed.code;
|
||||||
expect(actual).to.equal(expected);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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]}"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -34,7 +34,8 @@ describe('ScriptingDefinitionParser', () => {
|
|||||||
const expectedName = 'language';
|
const expectedName = 'language';
|
||||||
const info = new ProjectInformationStub();
|
const info = new ProjectInformationStub();
|
||||||
const definition = new ScriptingDefinitionBuilder()
|
const definition = new ScriptingDefinitionBuilder()
|
||||||
.withLanguage(languageText).construct();
|
.withLanguage(languageText)
|
||||||
|
.construct();
|
||||||
const parserMock = mockEnumParser(expectedName, languageText, expectedLanguage);
|
const parserMock = mockEnumParser(expectedName, languageText, expectedLanguage);
|
||||||
// act
|
// act
|
||||||
const actual = parseScriptingDefinition(definition, info, new Date(), parserMock);
|
const actual = parseScriptingDefinition(definition, info, new Date(), parserMock);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
|||||||
@@ -3,16 +3,15 @@ import 'mocha';
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { ScriptCodeStub } from '../stubs/ScriptCodeStub';
|
||||||
|
|
||||||
describe('Script', () => {
|
describe('Script', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
describe('scriptCode', () => {
|
describe('scriptCode', () => {
|
||||||
it('sets as expected', () => {
|
it('sets as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const name = 'test-script';
|
const expected = new ScriptCodeStub();
|
||||||
const expected = new ScriptCode(name, 'expected-execute', 'expected-revert');
|
|
||||||
const sut = new ScriptBuilder()
|
const sut = new ScriptBuilder()
|
||||||
.withCode(expected)
|
.withCode(expected)
|
||||||
.build();
|
.build();
|
||||||
@@ -110,12 +109,14 @@ describe('Script', () => {
|
|||||||
|
|
||||||
class ScriptBuilder {
|
class ScriptBuilder {
|
||||||
private name = 'test-script';
|
private name = 'test-script';
|
||||||
private code: IScriptCode = new ScriptCode(this.name, 'code', 'revert-code');
|
private code: IScriptCode = new ScriptCodeStub();
|
||||||
private level = RecommendationLevel.Standard;
|
private level = RecommendationLevel.Standard;
|
||||||
private documentationUrls: readonly string[] = undefined;
|
private documentationUrls: readonly string[] = undefined;
|
||||||
|
|
||||||
public withCodes(code: string, revertCode = ''): ScriptBuilder {
|
public withCodes(code: string, revertCode = ''): ScriptBuilder {
|
||||||
this.code = new ScriptCode(this.name, code, revertCode);
|
this.code = new ScriptCodeStub()
|
||||||
|
.withExecute(code)
|
||||||
|
.withRevert(revertCode);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'mocha';
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { LanguageSyntaxStub } from '../stubs/LanguageSyntaxStub';
|
||||||
|
|
||||||
describe('ScriptCode', () => {
|
describe('ScriptCode', () => {
|
||||||
describe('scriptName', () => {
|
describe('scriptName', () => {
|
||||||
@@ -10,7 +12,9 @@ describe('ScriptCode', () => {
|
|||||||
const expectedError = 'name is undefined';
|
const expectedError = 'name is undefined';
|
||||||
const name = undefined;
|
const name = undefined;
|
||||||
// act
|
// act
|
||||||
const act = () => new ScriptCode(name, 'non-empty-code', '');
|
const act = () => new ScriptCodeBuilder()
|
||||||
|
.withName(name)
|
||||||
|
.build();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
@@ -48,7 +52,11 @@ describe('ScriptCode', () => {
|
|||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
it(testCase.name, () => {
|
it(testCase.name, () => {
|
||||||
// act
|
// 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
|
// assert
|
||||||
expect(act).to.throw(testCase.expectedError);
|
expect(act).to.throw(testCase.expectedError);
|
||||||
});
|
});
|
||||||
@@ -72,15 +80,21 @@ describe('ScriptCode', () => {
|
|||||||
// act
|
// act
|
||||||
const actions = [];
|
const actions = [];
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
const substituteScriptName = (name) => testCase.expectedMessage.replace('$scriptName', name);
|
const substituteScriptName = (name: string) => testCase.expectedMessage.replace('$scriptName', name);
|
||||||
actions.push(...[
|
actions.push(...[
|
||||||
{
|
{
|
||||||
act: () => new ScriptCode(scriptName, testCase.code, undefined),
|
act: () => new ScriptCodeBuilder()
|
||||||
|
.withName(scriptName)
|
||||||
|
.withExecute(testCase.code)
|
||||||
|
.build(),
|
||||||
testName: `execute: ${testCase.testName}`,
|
testName: `execute: ${testCase.testName}`,
|
||||||
expectedMessage: substituteScriptName(scriptName),
|
expectedMessage: substituteScriptName(scriptName),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
act: () => new ScriptCode(scriptName, 'valid code', testCase.code),
|
act: () => new ScriptCodeBuilder()
|
||||||
|
.withName(scriptName)
|
||||||
|
.withRevert(testCase.code)
|
||||||
|
.build(),
|
||||||
testName: `revert: ${testCase.testName}`,
|
testName: `revert: ${testCase.testName}`,
|
||||||
expectedMessage: substituteScriptName(`${scriptName} (revert)`),
|
expectedMessage: substituteScriptName(`${scriptName} (revert)`),
|
||||||
},
|
},
|
||||||
@@ -96,26 +110,29 @@ describe('ScriptCode', () => {
|
|||||||
});
|
});
|
||||||
describe('sets as expected with valid "execute" or "revert"', () => {
|
describe('sets as expected with valid "execute" or "revert"', () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const syntax = new LanguageSyntaxStub()
|
||||||
|
.withCommonCodeParts(')', 'else', '(')
|
||||||
|
.withCommentDelimiters('#', '//');
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{
|
{
|
||||||
testName: 'code is a valid string',
|
testName: 'code is a valid string',
|
||||||
code: 'valid code',
|
code: 'valid code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: 'code consists of frequent code parts',
|
testName: 'code consists of common code parts',
|
||||||
code: ') else (',
|
code: syntax.commonCodeParts.join(' '),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: 'code is a frequent code part',
|
testName: 'code is a common code part',
|
||||||
code: ')',
|
code: syntax.commonCodeParts[0],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: 'code with duplicated comment lines (::)',
|
testName: `code with duplicated comment lines (${syntax.commentDelimiters[0]})`,
|
||||||
code: ':: comment\n:: comment',
|
code: `${syntax.commentDelimiters[0]} comment\n${syntax.commentDelimiters[0]} comment`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: 'code with duplicated comment lines (REM)',
|
testName: `code with duplicated comment lines (${syntax.commentDelimiters[1]})`,
|
||||||
code: 'REM comment\nREM comment',
|
code: `${syntax.commentDelimiters[1]} comment\n${syntax.commentDelimiters[1]} comment`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
// act
|
// act
|
||||||
@@ -124,12 +141,20 @@ describe('ScriptCode', () => {
|
|||||||
actions.push(...[
|
actions.push(...[
|
||||||
{
|
{
|
||||||
testName: `execute: ${testCase.testName}`,
|
testName: `execute: ${testCase.testName}`,
|
||||||
act: () => createSut(testCase.code),
|
act: () =>
|
||||||
|
new ScriptCodeBuilder()
|
||||||
|
.withSyntax(syntax)
|
||||||
|
.withExecute(testCase.code)
|
||||||
|
.build(),
|
||||||
expect: (sut: IScriptCode) => sut.execute === testCase.code,
|
expect: (sut: IScriptCode) => sut.execute === testCase.code,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: `revert: ${testCase.testName}`,
|
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,
|
expect: (sut: IScriptCode) => sut.revert === testCase.code,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -145,6 +170,34 @@ describe('ScriptCode', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createSut(code: string, revert = ''): ScriptCode {
|
class ScriptCodeBuilder {
|
||||||
return new ScriptCode('test-code', code, revert);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { expect } from 'chai';
|
|||||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { getEnumValues } from '@/application/Common/Enum';
|
import { getEnumValues } from '@/application/Common/Enum';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
|
|
||||||
describe('ScriptingDefinition', () => {
|
describe('ScriptingDefinition', () => {
|
||||||
describe('language', () => {
|
describe('language', () => {
|
||||||
@@ -38,7 +37,7 @@ describe('ScriptingDefinition', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const testCases = new Map<ScriptingLanguage, string>([
|
const testCases = new Map<ScriptingLanguage, string>([
|
||||||
[ScriptingLanguage.batchfile, 'bat'],
|
[ScriptingLanguage.batchfile, 'bat'],
|
||||||
[ScriptingLanguage.bash, 'sh'],
|
[ScriptingLanguage.shellscript, 'sh'],
|
||||||
]);
|
]);
|
||||||
Array.from(testCases.entries()).forEach((test) => {
|
Array.from(testCases.entries()).forEach((test) => {
|
||||||
const language = test[0];
|
const language = test[0];
|
||||||
@@ -108,7 +107,7 @@ describe('ScriptingDefinition', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
class ScriptingDefinitionBuilder {
|
class ScriptingDefinitionBuilder {
|
||||||
private language = ScriptingLanguage.bash;
|
private language = ScriptingLanguage.shellscript;
|
||||||
private startCode = 'REM start-code';
|
private startCode = 'REM start-code';
|
||||||
private endCode = 'REM end-code';
|
private endCode = 'REM end-code';
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ describe('AsyncLazy', () => {
|
|||||||
expect(results).to.deep.equal([1, 1, 1, 1, 1]);
|
expect(results).to.deep.equal([1, 1, 1, 1, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when running long-running task paralelly', async () => {
|
it('when running long-running task in parallel', async () => {
|
||||||
const sleep = (time: number) => new Promise(((resolve) => setTimeout(resolve, time)));
|
const sleepAsync = (time: number) => new Promise(((resolve) => setTimeout(resolve, time)));
|
||||||
const sut = new AsyncLazy(async () => {
|
const sut = new AsyncLazy(async () => {
|
||||||
await sleep(100);
|
await sleepAsync(100);
|
||||||
totalExecuted++;
|
totalExecuted++;
|
||||||
return Promise.resolve(totalExecuted);
|
return Promise.resolve(totalExecuted);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,81 +1,87 @@
|
|||||||
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
|
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import { EventHandler } from '@/infrastructure/Events/ISignal';
|
||||||
|
|
||||||
|
|
||||||
describe('Signal', () => {
|
describe('Signal', () => {
|
||||||
class ReceiverMock {
|
class ObserverMock {
|
||||||
public onRecieveCalls = new Array<number>();
|
public readonly onReceiveCalls = new Array<number>();
|
||||||
public onReceive(arg: number): void { this.onRecieveCalls.push(arg); }
|
public readonly callbacks = new Array<EventHandler<number>>();
|
||||||
|
public readonly subscription: IEventSubscription;
|
||||||
|
constructor(subject: ISignal<number>) {
|
||||||
|
this.callbacks.push((arg) => this.onReceiveCalls.push(arg));
|
||||||
|
this.subscription = subject.on((arg) => this.callbacks.forEach((action) => action(arg)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let signal: Signal<number>;
|
let signal: Signal<number>;
|
||||||
beforeEach(() => signal = new Signal());
|
beforeEach(() => signal = new Signal());
|
||||||
|
describe('single observer', () => {
|
||||||
describe('single reciever', () => {
|
// arrange
|
||||||
let receiver: ReceiverMock;
|
let observer: ObserverMock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
receiver = new ReceiverMock();
|
observer = new ObserverMock(signal);
|
||||||
signal.on((arg) => receiver.onReceive(arg));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('notify() executes the callback', () => {
|
it('notify() executes the callback', () => {
|
||||||
|
// act
|
||||||
signal.notify(5);
|
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', () => {
|
it('notify() executes the callback with the payload', () => {
|
||||||
const expected = 5;
|
const expected = 5;
|
||||||
|
// act
|
||||||
signal.notify(expected);
|
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
|
||||||
describe('multiple recievers', () => {
|
observer.subscription.unsubscribe();
|
||||||
let receivers: ReceiverMock[];
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('notify() should execute all callbacks', () => {
|
|
||||||
signal.notify(5);
|
signal.notify(5);
|
||||||
receivers.forEach((receiver) => {
|
// assert
|
||||||
expect(receiver.onRecieveCalls).to.have.length(1);
|
expect(observer.onReceiveCalls).to.have.lengthOf(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('multiple observers', () => {
|
||||||
|
// arrange
|
||||||
|
let observers: ObserverMock[];
|
||||||
|
beforeEach(() => {
|
||||||
|
observers = [
|
||||||
|
new ObserverMock(signal), new ObserverMock(signal),
|
||||||
|
new ObserverMock(signal), new ObserverMock(signal),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
it('notify() should execute all callbacks', () => {
|
||||||
|
// act
|
||||||
|
signal.notify(5);
|
||||||
|
// assert
|
||||||
|
observers.forEach((observer) => {
|
||||||
|
expect(observer.onReceiveCalls).to.have.length(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
it('notify() should execute all callbacks with payload', () => {
|
it('notify() should execute all callbacks with payload', () => {
|
||||||
const expected = 5;
|
const expected = 5;
|
||||||
|
// act
|
||||||
signal.notify(expected);
|
signal.notify(expected);
|
||||||
receivers.forEach((receiver) => {
|
// assert
|
||||||
expect(receiver.onRecieveCalls).to.deep.equal([expected]);
|
observers.forEach((observer) => {
|
||||||
|
expect(observer.onReceiveCalls).to.deep.equal([expected]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('notify() executes in FIFO order', () => {
|
it('notify() executes in FIFO order', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedSequence = [0, 1, 2, 3];
|
const expectedSequence = [0, 1, 2, 3];
|
||||||
const actualSequence = new Array<number>();
|
const actualSequence = new Array<number>();
|
||||||
for (let i = 0; i < receivers.length; i++) {
|
for (let i = 0; i < observers.length; i++) {
|
||||||
receivers[i].onReceive = ((arg) => {
|
observers[i].callbacks.push(() => actualSequence.push(i));
|
||||||
actualSequence.push(i);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// act
|
// act
|
||||||
signal.notify(5);
|
signal.notify(5);
|
||||||
// assert
|
// assert
|
||||||
expect(actualSequence).to.deep.equal(expectedSequence);
|
expect(actualSequence).to.deep.equal(expectedSequence);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
19
tests/unit/stubs/CategoryCollectionParseContextStub.ts
Normal file
19
tests/unit/stubs/CategoryCollectionParseContextStub.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,11 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
|||||||
this.initialScript = script;
|
this.initialScript = script;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
public withTotalScripts(totalScripts: number) {
|
||||||
|
this.totalScripts = totalScripts;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public findCategory(categoryId: number): ICategory {
|
public findCategory(categoryId: number): ICategory {
|
||||||
return this.getAllCategories().find(
|
return this.getAllCategories().find(
|
||||||
(category) => category.id === categoryId);
|
(category) => category.id === categoryId);
|
||||||
|
|||||||
25
tests/unit/stubs/FunctionDataStub.ts
Normal file
25
tests/unit/stubs/FunctionDataStub.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
tests/unit/stubs/LanguageSyntaxStub.ts
Normal file
15
tests/unit/stubs/LanguageSyntaxStub.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
tests/unit/stubs/ScriptCodeStub.ts
Normal file
15
tests/unit/stubs/ScriptCodeStub.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||||
|
|
||||||
|
|||||||
@@ -27,39 +27,30 @@ export class ScriptDataStub implements ScriptData {
|
|||||||
public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
|
public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
|
||||||
public docs = ['hello.com'];
|
public docs = ['hello.com'];
|
||||||
|
|
||||||
private constructor() { }
|
|
||||||
|
|
||||||
public withName(name: string): ScriptDataStub {
|
public withName(name: string): ScriptDataStub {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withDocs(docs: string[]): ScriptDataStub {
|
public withDocs(docs: string[]): ScriptDataStub {
|
||||||
this.docs = docs;
|
this.docs = docs;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withCode(code: string): ScriptDataStub {
|
public withCode(code: string): ScriptDataStub {
|
||||||
this.code = code;
|
this.code = code;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withRevertCode(revertCode: string): ScriptDataStub {
|
public withRevertCode(revertCode: string): ScriptDataStub {
|
||||||
this.revertCode = revertCode;
|
this.revertCode = revertCode;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withMockCall(): ScriptDataStub {
|
public withMockCall(): ScriptDataStub {
|
||||||
this.call = { function: 'func', parameters: [] };
|
this.call = { function: 'func', parameters: [] };
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withCall(call: ScriptFunctionCallData): ScriptDataStub {
|
public withCall(call: ScriptFunctionCallData): ScriptDataStub {
|
||||||
this.call = call;
|
this.call = call;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public withRecommend(recommend: string): ScriptDataStub {
|
public withRecommend(recommend: string): ScriptDataStub {
|
||||||
this.recommend = recommend;
|
this.recommend = recommend;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -15,4 +15,8 @@ export class ScriptingDefinitionStub implements IScriptingDefinition {
|
|||||||
this.endCode = endCode;
|
this.endCode = endCode;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
public withLanguage(language: ScriptingLanguage): ScriptingDefinitionStub {
|
||||||
|
this.language = language;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user