add auto-highlighting of selected/updated code
This commit is contained in:
@@ -1,30 +1,38 @@
|
|||||||
|
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
|
import { CodePosition } from './Position/CodePosition';
|
||||||
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
||||||
import { UserScriptGenerator } from './UserScriptGenerator';
|
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
import { IApplicationCode } from './IApplicationCode';
|
import { IApplicationCode } from './IApplicationCode';
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||||
|
|
||||||
export class ApplicationCode implements IApplicationCode {
|
export class ApplicationCode implements IApplicationCode {
|
||||||
public readonly changed = new Signal<string>();
|
public readonly changed = new Signal<ICodeChangedEvent>();
|
||||||
public current: string;
|
public current: string;
|
||||||
|
|
||||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator();
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
userSelection: IUserSelection,
|
userSelection: IUserSelection,
|
||||||
private readonly version: string) {
|
private readonly version: string,
|
||||||
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
||||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||||
if (!version) { throw new Error('version is null or undefined'); }
|
if (!version) { throw new Error('version is null or undefined'); }
|
||||||
this.generator = new UserScriptGenerator();
|
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||||
this.setCode(userSelection.selectedScripts);
|
this.setCode(userSelection.selectedScripts);
|
||||||
userSelection.changed.on((scripts) => {
|
userSelection.changed.on((scripts) => {
|
||||||
this.setCode(scripts);
|
this.setCode(scripts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCode(scripts: ReadonlyArray<SelectedScript>) {
|
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||||
this.current = scripts.length === 0 ? '' : this.generator.buildCode(scripts, this.version);
|
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||||
this.changed.notify(this.current);
|
const code = this.generator.buildCode(scripts, this.version);
|
||||||
|
this.current = code.code;
|
||||||
|
this.scriptPositions = code.scriptPositions;
|
||||||
|
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||||
|
this.changed.notify(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/application/State/Code/Event/CodeChangedEvent.ts
Normal file
64
src/application/State/Code/Event/CodeChangedEvent.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
|
public readonly code: string;
|
||||||
|
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
|
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
|
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
|
private readonly scripts: Map<IScript, ICodePosition>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
code: string,
|
||||||
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
scripts: Map<SelectedScript, ICodePosition>) {
|
||||||
|
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
||||||
|
this.code = code;
|
||||||
|
const newScripts = Array.from(scripts.keys());
|
||||||
|
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
||||||
|
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
||||||
|
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
||||||
|
this.scripts = new Map<IScript, ICodePosition>();
|
||||||
|
scripts.forEach((position, selection) => {
|
||||||
|
this.scripts.set(selection.script, position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
return this.scripts.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||||
|
return this.scripts.get(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||||
|
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||||
|
for (const position of positions) {
|
||||||
|
if (position.endLine > totalLines) {
|
||||||
|
throw new Error(`script end line (${position.endLine}) is out of range.` +
|
||||||
|
`(total code lines: ${totalLines}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChangedScripts(
|
||||||
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
|
||||||
|
return newScripts
|
||||||
|
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||||
|
&& oldScript.revert !== newScript.revert ))
|
||||||
|
.map((selection) => selection.script);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectIfNotExists(
|
||||||
|
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||||
|
test: ReadonlyArray<SelectedScript>) {
|
||||||
|
return selectableContainer
|
||||||
|
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||||
|
.map((selection) => selection.script);
|
||||||
|
}
|
||||||
11
src/application/State/Code/Event/ICodeChangedEvent.ts
Normal file
11
src/application/State/Code/Event/ICodeChangedEvent.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
|
export interface ICodeChangedEvent {
|
||||||
|
readonly code: string;
|
||||||
|
addedScripts: ReadonlyArray<IScript>;
|
||||||
|
removedScripts: ReadonlyArray<IScript>;
|
||||||
|
changedScripts: ReadonlyArray<IScript>;
|
||||||
|
isEmpty(): boolean;
|
||||||
|
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||||
|
}
|
||||||
@@ -4,8 +4,20 @@ const TotalFunctionSeparatorChars = 58;
|
|||||||
export class CodeBuilder {
|
export class CodeBuilder {
|
||||||
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)
|
||||||
|
public get currentLine(): number {
|
||||||
|
return this.lines.length;
|
||||||
|
}
|
||||||
|
|
||||||
public appendLine(code?: string): CodeBuilder {
|
public appendLine(code?: string): CodeBuilder {
|
||||||
this.lines.push(code);
|
if (!code) {
|
||||||
|
this.lines.push('');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
|
for (const line of lines) {
|
||||||
|
this.lines.push(line);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
7
src/application/State/Code/Generation/IUserScript.ts
Normal file
7
src/application/State/Code/Generation/IUserScript.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
|
export interface IUserScript {
|
||||||
|
code: string;
|
||||||
|
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
export interface IUserScriptGenerator {
|
export interface IUserScriptGenerator {
|
||||||
buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string;
|
buildCode(
|
||||||
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
version: string): IUserScript;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
import { CodeBuilder } from './CodeBuilder';
|
import { CodeBuilder } from './CodeBuilder';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
|
|
||||||
export const adminRightsScript = {
|
export const adminRightsScript = {
|
||||||
name: 'Ensure admin privileges',
|
name: 'Ensure admin privileges',
|
||||||
@@ -13,22 +16,51 @@ export const adminRightsScript = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||||
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string {
|
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): IUserScript {
|
||||||
if (!selectedScripts) { throw new Error('scripts is undefined'); }
|
if (!selectedScripts) { throw new Error('scripts is undefined'); }
|
||||||
if (!selectedScripts.length) { throw new Error('scripts are empty'); }
|
|
||||||
if (!version) { throw new Error('version is undefined'); }
|
if (!version) { throw new Error('version is undefined'); }
|
||||||
const builder = new CodeBuilder()
|
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||||
.appendLine('@echo off')
|
if (!selectedScripts.length) {
|
||||||
.appendCommentLine(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)
|
return { code: '', scriptPositions };
|
||||||
.appendFunction(adminRightsScript.name, adminRightsScript.code).appendLine();
|
|
||||||
for (const selection of selectedScripts) {
|
|
||||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
|
||||||
const code = selection.revert ? selection.script.revertCode : selection.script.code;
|
|
||||||
builder.appendFunction(name, code).appendLine();
|
|
||||||
}
|
}
|
||||||
return builder.appendLine()
|
const builder = initializeCode(version);
|
||||||
.appendLine('pause')
|
for (const selection of selectedScripts) {
|
||||||
.appendLine('exit /b 0')
|
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||||
.toString();
|
}
|
||||||
|
const code = finalizeCode(builder);
|
||||||
|
return { code, scriptPositions };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initializeCode(version: string): CodeBuilder {
|
||||||
|
return new CodeBuilder()
|
||||||
|
.appendLine('@echo off')
|
||||||
|
.appendCommentLine(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)
|
||||||
|
.appendFunction(adminRightsScript.name, adminRightsScript.code)
|
||||||
|
.appendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeCode(builder: CodeBuilder): string {
|
||||||
|
return builder.appendLine()
|
||||||
|
.appendLine('pause')
|
||||||
|
.appendLine('exit /b 0')
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSelection(
|
||||||
|
selection: SelectedScript,
|
||||||
|
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||||
|
builder: CodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||||
|
const startPosition = builder.currentLine + 1;
|
||||||
|
appendCode(selection, builder);
|
||||||
|
const endPosition = builder.currentLine - 1;
|
||||||
|
builder.appendLine();
|
||||||
|
scriptPositions.set(selection, new CodePosition(startPosition, endPosition));
|
||||||
|
return scriptPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendCode(selection: SelectedScript, builder: CodeBuilder) {
|
||||||
|
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||||
|
const scriptCode = selection.revert ? selection.script.revertCode : selection.script.code;
|
||||||
|
builder.appendFunction(name, scriptCode);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
|
|
||||||
export interface IApplicationCode {
|
export interface IApplicationCode {
|
||||||
readonly changed: ISignal<string>;
|
readonly changed: ISignal<ICodeChangedEvent>;
|
||||||
readonly current: string;
|
readonly current: string;
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/application/State/Code/Position/CodePosition.ts
Normal file
24
src/application/State/Code/Position/CodePosition.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { ICodePosition } from './ICodePosition';
|
||||||
|
export class CodePosition implements ICodePosition {
|
||||||
|
|
||||||
|
public get totalLines(): number {
|
||||||
|
return this.endLine - this.startLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly startLine: number,
|
||||||
|
public readonly endLine: number) {
|
||||||
|
if (startLine < 0) {
|
||||||
|
throw new Error('Code cannot start in a negative line');
|
||||||
|
}
|
||||||
|
if (endLine < 0) {
|
||||||
|
throw new Error('Code cannot end in a negative line');
|
||||||
|
}
|
||||||
|
if (endLine === startLine) {
|
||||||
|
throw new Error('Empty code');
|
||||||
|
}
|
||||||
|
if (endLine < startLine) {
|
||||||
|
throw new Error('End line cannot be less than start line');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/application/State/Code/Position/ICodePosition.ts
Normal file
5
src/application/State/Code/Position/ICodePosition.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ICodePosition {
|
||||||
|
readonly startLine: number;
|
||||||
|
readonly endLine: number;
|
||||||
|
readonly totalLines: number;
|
||||||
|
}
|
||||||
@@ -6,11 +6,13 @@ export interface IUserSelection {
|
|||||||
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
||||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||||
readonly totalSelected: number;
|
readonly totalSelected: number;
|
||||||
|
removeAllInCategory(categoryId: number): void;
|
||||||
|
addAllInCategory(categoryId: number): void;
|
||||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
removeSelectedScript(scriptId: string): void;
|
removeSelectedScript(scriptId: string): void;
|
||||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||||
isSelected(script: IScript): boolean;
|
isSelected(scriptId: string): boolean;
|
||||||
selectAll(): void;
|
selectAll(): void;
|
||||||
deselectAll(): void;
|
deselectAll(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import { IRepository } from '@/infrastructure/Repository/IRepository';
|
|||||||
|
|
||||||
export class UserSelection implements IUserSelection {
|
export class UserSelection implements IUserSelection {
|
||||||
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
|
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
|
||||||
private readonly scripts: IRepository<string, SelectedScript> = new InMemoryRepository<string, SelectedScript>();
|
private readonly scripts: IRepository<string, SelectedScript>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly app: IApplication,
|
private readonly app: IApplication,
|
||||||
/** Initially selected scripts */
|
/** Initially selected scripts */
|
||||||
selectedScripts: ReadonlyArray<IScript>) {
|
selectedScripts: ReadonlyArray<IScript>) {
|
||||||
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
if (selectedScripts && selectedScripts.length > 0) {
|
if (selectedScripts && selectedScripts.length > 0) {
|
||||||
for (const script of selectedScripts) {
|
for (const script of selectedScripts) {
|
||||||
const selected = new SelectedScript(script, false);
|
const selected = new SelectedScript(script, false);
|
||||||
@@ -22,6 +23,33 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeAllInCategory(categoryId: number): void {
|
||||||
|
const category = this.app.findCategory(categoryId);
|
||||||
|
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||||
|
.filter((script) => this.scripts.exists(script.id));
|
||||||
|
if (!scriptsToRemove.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const script of scriptsToRemove) {
|
||||||
|
this.scripts.removeItem(script.id);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
public addAllInCategory(categoryId: number): void {
|
||||||
|
const category = this.app.findCategory(categoryId);
|
||||||
|
const scriptsToAdd = category.getAllScriptsRecursively()
|
||||||
|
.filter((script) => !this.scripts.exists(script.id));
|
||||||
|
if (!scriptsToAdd.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const script of scriptsToAdd) {
|
||||||
|
const selectedScript = new SelectedScript(script, false);
|
||||||
|
this.scripts.addItem(selectedScript);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
const script = this.app.findScript(scriptId);
|
const script = this.app.findScript(scriptId);
|
||||||
if (!script) {
|
if (!script) {
|
||||||
@@ -44,8 +72,8 @@ export class UserSelection implements IUserSelection {
|
|||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSelected(script: IScript): boolean {
|
public isSelected(scriptId: string): boolean {
|
||||||
return this.scripts.exists(script.id);
|
return this.scripts.exists(scriptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get users scripts based on his/her selections */
|
/** Get users scripts based on his/her selections */
|
||||||
|
|||||||
@@ -3,15 +3,7 @@ import { IScript } from './IScript';
|
|||||||
import { ICategory } from './ICategory';
|
import { ICategory } from './ICategory';
|
||||||
|
|
||||||
export class Category extends BaseEntity<number> implements ICategory {
|
export class Category extends BaseEntity<number> implements ICategory {
|
||||||
private static validate(category: ICategory) {
|
private allSubScripts: ReadonlyArray<IScript> = undefined;
|
||||||
if (!category.name) {
|
|
||||||
throw new Error('name is null or empty');
|
|
||||||
}
|
|
||||||
if ((!category.subCategories || category.subCategories.length === 0) &&
|
|
||||||
(!category.scripts || category.scripts.length === 0)) {
|
|
||||||
throw new Error('A category must have at least one sub-category or scripts');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: number,
|
id: number,
|
||||||
@@ -20,6 +12,27 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||||
public readonly scripts?: ReadonlyArray<IScript>) {
|
public readonly scripts?: ReadonlyArray<IScript>) {
|
||||||
super(id);
|
super(id);
|
||||||
Category.validate(this);
|
validateCategory(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllScriptsRecursively(): readonly IScript[] {
|
||||||
|
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
||||||
|
return [
|
||||||
|
...category.scripts,
|
||||||
|
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCategory(category: ICategory) {
|
||||||
|
if (!category.name) {
|
||||||
|
throw new Error('undefined or empty name');
|
||||||
|
}
|
||||||
|
if ((!category.subCategories || category.subCategories.length === 0) &&
|
||||||
|
(!category.scripts || category.scripts.length === 0)) {
|
||||||
|
throw new Error('A category must have at least one sub-category or script');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ICategory extends IEntity<number>, IDocumentable {
|
|||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
readonly subCategories?: ReadonlyArray<ICategory>;
|
||||||
readonly scripts?: ReadonlyArray<IScript>;
|
readonly scripts?: ReadonlyArray<IScript>;
|
||||||
|
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { IEntity } from '../infrastructure/Entity/IEntity';
|
export { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IApplication } from './../../../domain/IApplication';
|
import { IApplication } from './../../../domain/IApplication';
|
||||||
import { ICategory, IScript } from '@/domain/ICategory';
|
import { ICategory, IScript } from '@/domain/ICategory';
|
||||||
import { INode } from './SelectableTree/INode';
|
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||||
|
|
||||||
export function parseAllCategories(app: IApplication): INode[] | undefined {
|
export function parseAllCategories(app: IApplication): INode[] | undefined {
|
||||||
const nodes = new Array<INode>();
|
const nodes = new Array<INode>();
|
||||||
@@ -23,9 +23,15 @@ export function parseSingleCategory(categoryId: number, app: IApplication): INod
|
|||||||
export function getScriptNodeId(script: IScript): string {
|
export function getScriptNodeId(script: IScript): string {
|
||||||
return script.id;
|
return script.id;
|
||||||
}
|
}
|
||||||
|
export function getScriptId(nodeId: string): string {
|
||||||
|
return nodeId;
|
||||||
|
}
|
||||||
|
export function getCategoryId(nodeId: string): number {
|
||||||
|
return +nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
export function getCategoryNodeId(category: ICategory): string {
|
export function getCategoryNodeId(category: ICategory): string {
|
||||||
return `Category${category.id}`;
|
return `${category.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategoryRecursively(
|
function parseCategoryRecursively(
|
||||||
@@ -64,6 +70,7 @@ function convertCategoryToNode(
|
|||||||
category: ICategory, children: readonly INode[]): INode {
|
category: ICategory, children: readonly INode[]): INode {
|
||||||
return {
|
return {
|
||||||
id: getCategoryNodeId(category),
|
id: getCategoryNodeId(category),
|
||||||
|
type: NodeType.Category,
|
||||||
text: category.name,
|
text: category.name,
|
||||||
children,
|
children,
|
||||||
documentationUrls: category.documentationUrls,
|
documentationUrls: category.documentationUrls,
|
||||||
@@ -74,6 +81,7 @@ function convertCategoryToNode(
|
|||||||
function convertScriptToNode(script: IScript): INode {
|
function convertScriptToNode(script: IScript): INode {
|
||||||
return {
|
return {
|
||||||
id: getScriptNodeId(script),
|
id: getScriptNodeId(script),
|
||||||
|
type: NodeType.Script,
|
||||||
text: script.name,
|
text: script.name,
|
||||||
children: undefined,
|
children: undefined,
|
||||||
documentationUrls: script.documentationUrls,
|
documentationUrls: script.documentationUrls,
|
||||||
|
|||||||
@@ -24,10 +24,11 @@
|
|||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
|
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
|
||||||
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
|
||||||
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId } from './ScriptNodeParser';
|
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId, getCategoryId, getScriptId } from './ScriptNodeParser';
|
||||||
import SelectableTree, { FilterPredicate } from './SelectableTree/SelectableTree.vue';
|
import SelectableTree from './SelectableTree/SelectableTree.vue';
|
||||||
import { INode } from './SelectableTree/INode';
|
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -53,15 +54,17 @@
|
|||||||
await this.initializeNodesAsync(this.categoryId);
|
await this.initializeNodesAsync(this.categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toggleNodeSelectionAsync(node: INode) {
|
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
||||||
if (node.children != null && node.children.length > 0) {
|
|
||||||
return; // only interested in script nodes
|
|
||||||
}
|
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
if (!this.selectedNodeIds.some((id) => id === node.id)) {
|
switch (event.node.type) {
|
||||||
state.selection.addSelectedScript(node.id, false);
|
case NodeType.Category:
|
||||||
} else {
|
this.toggleCategoryNodeSelection(event, state);
|
||||||
state.selection.removeSelectedScript(node.id);
|
break;
|
||||||
|
case NodeType.Script:
|
||||||
|
this.toggleScriptNodeSelection(event, state);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown node type: ${event.node.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +100,24 @@
|
|||||||
this.filterText = result.query;
|
this.filterText = result.query;
|
||||||
this.filtered = result;
|
this.filtered = result;
|
||||||
}
|
}
|
||||||
|
private toggleCategoryNodeSelection(event: INodeSelectedEvent, state: IApplicationState): void {
|
||||||
|
const categoryId = getCategoryId(event.node.id);
|
||||||
|
if (event.isSelected) {
|
||||||
|
state.selection.addAllInCategory(categoryId);
|
||||||
|
} else {
|
||||||
|
state.selection.removeAllInCategory(categoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private toggleScriptNodeSelection(event: INodeSelectedEvent, state: IApplicationState): void {
|
||||||
|
const scriptId = getScriptId(event.node.id);
|
||||||
|
const actualToggleState = state.selection.isSelected(scriptId);
|
||||||
|
const targetToggleState = event.isSelected;
|
||||||
|
if (targetToggleState && !actualToggleState) {
|
||||||
|
state.selection.addSelectedScript(scriptId, false);
|
||||||
|
} else if (!targetToggleState && actualToggleState) {
|
||||||
|
state.selection.removeSelectedScript(scriptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { INode } from './Node/INode';
|
||||||
|
|
||||||
|
export interface INodeSelectedEvent {
|
||||||
|
isSelected: boolean;
|
||||||
|
node: INode;
|
||||||
|
}
|
||||||
@@ -12,16 +12,25 @@ declare module 'liquor-tree' {
|
|||||||
setModel(nodes: ReadonlyArray<ILiquorTreeNewNode>): void;
|
setModel(nodes: ReadonlyArray<ILiquorTreeNewNode>): void;
|
||||||
}
|
}
|
||||||
interface ICustomLiquorTreeData {
|
interface ICustomLiquorTreeData {
|
||||||
|
type: number;
|
||||||
documentationUrls: ReadonlyArray<string>;
|
documentationUrls: ReadonlyArray<string>;
|
||||||
isReversible: boolean;
|
isReversible: boolean;
|
||||||
}
|
}
|
||||||
|
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
||||||
|
export interface ILiquorTreeNodeState {
|
||||||
|
checked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILiquorTreeNode {
|
||||||
|
id: string;
|
||||||
|
data: ICustomLiquorTreeData;
|
||||||
|
children: ReadonlyArray<ILiquorTreeNode> | undefined;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Returned from Node tree view events.
|
* Returned from Node tree view events.
|
||||||
* See constructor in https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
* See constructor in https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
||||||
*/
|
*/
|
||||||
export interface ILiquorTreeExistingNode {
|
export interface ILiquorTreeExistingNode extends ILiquorTreeNode {
|
||||||
id: string;
|
|
||||||
data: ILiquorTreeNodeData;
|
data: ILiquorTreeNodeData;
|
||||||
states: ILiquorTreeNodeState | undefined;
|
states: ILiquorTreeNodeState | undefined;
|
||||||
children: ReadonlyArray<ILiquorTreeExistingNode> | undefined;
|
children: ReadonlyArray<ILiquorTreeExistingNode> | undefined;
|
||||||
@@ -31,12 +40,10 @@ declare module 'liquor-tree' {
|
|||||||
* Sent to liquor tree to define of new nodes.
|
* Sent to liquor tree to define of new nodes.
|
||||||
* https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
* https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
||||||
*/
|
*/
|
||||||
export interface ILiquorTreeNewNode {
|
export interface ILiquorTreeNewNode extends ILiquorTreeNode {
|
||||||
id: string;
|
|
||||||
text: string;
|
text: string;
|
||||||
state: ILiquorTreeNodeState | undefined;
|
state: ILiquorTreeNodeState | undefined;
|
||||||
children: ReadonlyArray<ILiquorTreeNewNode> | undefined;
|
children: ReadonlyArray<ILiquorTreeNewNode> | undefined;
|
||||||
data: ICustomLiquorTreeData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://amsik.github.io/liquor-tree/#Component-Options
|
// https://amsik.github.io/liquor-tree/#Component-Options
|
||||||
@@ -47,13 +54,8 @@ declare module 'liquor-tree' {
|
|||||||
autoCheckChildren: boolean;
|
autoCheckChildren: boolean;
|
||||||
parentSelect: boolean;
|
parentSelect: boolean;
|
||||||
keyboardNavigation: boolean;
|
keyboardNavigation: boolean;
|
||||||
deletion: (node: ILiquorTreeExistingNode) => void;
|
|
||||||
filter: ILiquorTreeFilter;
|
filter: ILiquorTreeFilter;
|
||||||
}
|
deletion(node: ILiquorTreeNode): boolean;
|
||||||
|
|
||||||
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
|
||||||
interface ILiquorTreeNodeState {
|
|
||||||
checked: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
|
interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
|
||||||
@@ -61,15 +63,7 @@ declare module 'liquor-tree' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
|
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
|
||||||
interface ILiquorTreeOptions {
|
export interface ILiquorTreeFilter {
|
||||||
checkbox: boolean;
|
|
||||||
checkOnSelect: boolean;
|
|
||||||
filter: ILiquorTreeFilter;
|
|
||||||
deletion(node: ILiquorTreeNewNode): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
|
|
||||||
interface ILiquorTreeFilter {
|
|
||||||
emptyText: string;
|
emptyText: string;
|
||||||
matcher(query: string, node: ILiquorTreeExistingNode): boolean;
|
matcher(query: string, node: ILiquorTreeExistingNode): boolean;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ILiquorTreeOptions, ILiquorTreeFilter, ILiquorTreeNode } from 'liquor-tree';
|
||||||
|
|
||||||
|
export class LiquorTreeOptions implements ILiquorTreeOptions {
|
||||||
|
public multiple = true;
|
||||||
|
public checkbox = true;
|
||||||
|
public checkOnSelect = true;
|
||||||
|
/* For checkbox mode only. Children will have the same checked state as their parent.
|
||||||
|
This is false as it's handled manually to be able to batch select for performance + highlighting */
|
||||||
|
public autoCheckChildren = false;
|
||||||
|
public parentSelect = false;
|
||||||
|
public keyboardNavigation = true;
|
||||||
|
constructor(public filter: ILiquorTreeFilter) { }
|
||||||
|
public deletion(node: ILiquorTreeNode): boolean {
|
||||||
|
return false; // no op
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ILiquorTreeFilter, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||||
|
import { convertExistingToNode } from './NodeTranslator';
|
||||||
|
import { INode } from '../Node/INode';
|
||||||
|
|
||||||
|
export type FilterPredicate = (node: INode) => boolean;
|
||||||
|
|
||||||
|
export class NodePredicateFilter implements ILiquorTreeFilter {
|
||||||
|
public emptyText: string = '🕵️Hmm.. Can not see one 🧐';
|
||||||
|
constructor(private readonly filterPredicate: FilterPredicate) {
|
||||||
|
if (!filterPredicate) {
|
||||||
|
throw new Error('filterPredicate is undefined');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public matcher(query: string, node: ILiquorTreeExistingNode): boolean {
|
||||||
|
return this.filterPredicate(convertExistingToNode(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { ILiquorTreeExistingNode, ILiquorTreeNewNode, ILiquorTreeNodeState, ILiquorTreeNode } from 'liquor-tree';
|
||||||
|
import { NodeType } from './../Node/INode';
|
||||||
|
|
||||||
|
export function updateNodesCheckedState(
|
||||||
|
oldNodes: ReadonlyArray<ILiquorTreeExistingNode>,
|
||||||
|
selectedNodeIds: ReadonlyArray<string>): ReadonlyArray<ILiquorTreeNewNode> {
|
||||||
|
const result = new Array<ILiquorTreeNewNode>();
|
||||||
|
for (const oldNode of oldNodes) {
|
||||||
|
const newState = oldNode.states;
|
||||||
|
newState.checked = getNewCheckedState(oldNode, selectedNodeIds);
|
||||||
|
const newNode: ILiquorTreeNewNode = {
|
||||||
|
id: oldNode.id,
|
||||||
|
text: oldNode.data.text,
|
||||||
|
data: {
|
||||||
|
type: oldNode.data.type,
|
||||||
|
documentationUrls: oldNode.data.documentationUrls,
|
||||||
|
isReversible: oldNode.data.isReversible,
|
||||||
|
},
|
||||||
|
children: !oldNode.children ? [] : updateNodesCheckedState(oldNode.children, selectedNodeIds),
|
||||||
|
state: newState,
|
||||||
|
};
|
||||||
|
result.push(newNode);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNewCheckedState(
|
||||||
|
oldNode: ILiquorTreeNode,
|
||||||
|
selectedNodeIds: ReadonlyArray<string>): boolean {
|
||||||
|
switch (oldNode.data.type) {
|
||||||
|
case NodeType.Script:
|
||||||
|
return selectedNodeIds.some((id) => id === oldNode.id);
|
||||||
|
case NodeType.Category:
|
||||||
|
return parseAllSubScriptIds(oldNode).every((id) => selectedNodeIds.includes(id));
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown node type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAllSubScriptIds(categoryNode: ILiquorTreeNode): ReadonlyArray<string> {
|
||||||
|
if (categoryNode.data.type !== NodeType.Category) {
|
||||||
|
throw new Error('Not a category node');
|
||||||
|
}
|
||||||
|
if (!categoryNode.children) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const ids = new Array<string>();
|
||||||
|
for (const child of categoryNode.children) {
|
||||||
|
addNodeIds(child, ids);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNodeIds(node: ILiquorTreeNode, ids: string[]) {
|
||||||
|
switch (node.data.type) {
|
||||||
|
case NodeType.Script:
|
||||||
|
ids.push(node.id);
|
||||||
|
break;
|
||||||
|
case NodeType.Category:
|
||||||
|
const subCategoryIds = parseAllSubScriptIds(node);
|
||||||
|
ids.push(...subCategoryIds);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown node type');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ILiquorTreeNewNode, ILiquorTreeExistingNode } from 'liquor-tree';
|
import { ILiquorTreeNewNode, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||||
import { INode } from './INode';
|
import { INode } from './../Node/INode';
|
||||||
|
|
||||||
// Functions to translate INode to LiqourTree models and vice versa for anti-corruption
|
// Functions to translate INode to LiqourTree models and vice versa for anti-corruption
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ export function convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode):
|
|||||||
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
|
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
|
||||||
return {
|
return {
|
||||||
id: liquorTreeNode.id,
|
id: liquorTreeNode.id,
|
||||||
|
type: liquorTreeNode.data.type,
|
||||||
text: liquorTreeNode.data.text,
|
text: liquorTreeNode.data.text,
|
||||||
// selected: liquorTreeNode.states && liquorTreeNode.states.checked,
|
// selected: liquorTreeNode.states && liquorTreeNode.states.checked,
|
||||||
children: convertChildren(liquorTreeNode.children, convertExistingToNode),
|
children: convertChildren(liquorTreeNode.children, convertExistingToNode),
|
||||||
@@ -27,6 +28,7 @@ export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
|
|||||||
data: {
|
data: {
|
||||||
documentationUrls: node.documentationUrls,
|
documentationUrls: node.documentationUrls,
|
||||||
isReversible: node.isReversible,
|
isReversible: node.isReversible,
|
||||||
|
type: node.type,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
|
export enum NodeType {
|
||||||
|
Script,
|
||||||
|
Category,
|
||||||
|
}
|
||||||
|
|
||||||
export interface INode {
|
export interface INode {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly text: string;
|
readonly text: string;
|
||||||
readonly isReversible: boolean;
|
readonly isReversible: boolean;
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly documentationUrls: ReadonlyArray<string>;
|
||||||
readonly children?: ReadonlyArray<INode>;
|
readonly children?: ReadonlyArray<INode>;
|
||||||
|
readonly type: NodeType;
|
||||||
}
|
}
|
||||||
@@ -19,16 +19,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Emit, Watch } from 'vue-property-decorator';
|
import { Component, Prop, Vue, Emit, Watch } from 'vue-property-decorator';
|
||||||
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeOptions } from 'liquor-tree';
|
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeOptions } from 'liquor-tree';
|
||||||
import Node from './Node.vue';
|
import Node from './Node/Node.vue';
|
||||||
import { INode } from './INode';
|
import { INode, NodeType } from './Node/INode';
|
||||||
import { convertExistingToNode, toNewLiquorTreeNode } from './NodeTranslator';
|
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeTranslator';
|
||||||
export type FilterPredicate = (node: INode) => boolean;
|
import { INodeSelectedEvent } from './/INodeSelectedEvent';
|
||||||
|
import { updateNodesCheckedState, getNewCheckedState } from './LiquorTree/NodeStateUpdater';
|
||||||
|
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
|
||||||
|
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodePredicateFilter';
|
||||||
|
|
||||||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
LiquorTree,
|
LiquorTree,
|
||||||
Node,
|
Node,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class SelectableTree extends Vue {
|
export default class SelectableTree extends Vue {
|
||||||
@@ -38,7 +41,7 @@
|
|||||||
@Prop() public initialNodes?: ReadonlyArray<INode>;
|
@Prop() public initialNodes?: ReadonlyArray<INode>;
|
||||||
|
|
||||||
public initialLiquourTreeNodes?: ILiquorTreeNewNode[] = null;
|
public initialLiquourTreeNodes?: ILiquorTreeNewNode[] = null;
|
||||||
public liquorTreeOptions = this.getDefaults();
|
public liquorTreeOptions = new LiquorTreeOptions(new NodePredicateFilter((node) => this.filterPredicate(node)));
|
||||||
public convertExistingToNode = convertExistingToNode;
|
public convertExistingToNode = convertExistingToNode;
|
||||||
|
|
||||||
public mounted() {
|
public mounted() {
|
||||||
@@ -46,7 +49,7 @@
|
|||||||
const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node));
|
const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node));
|
||||||
if (this.selectedNodeIds) {
|
if (this.selectedNodeIds) {
|
||||||
recurseDown(initialNodes,
|
recurseDown(initialNodes,
|
||||||
(node) => node.state.checked = this.selectedNodeIds.includes(node.id));
|
(node) => node.state.checked = getNewCheckedState(node, this.selectedNodeIds));
|
||||||
}
|
}
|
||||||
this.initialLiquourTreeNodes = initialNodes;
|
this.initialLiquourTreeNodes = initialNodes;
|
||||||
} else {
|
} else {
|
||||||
@@ -58,7 +61,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
public nodeSelected(node: ILiquorTreeExistingNode) {
|
public nodeSelected(node: ILiquorTreeExistingNode) {
|
||||||
this.$emit('nodeSelected', convertExistingToNode(node));
|
const event: INodeSelectedEvent = {
|
||||||
|
node: convertExistingToNode(node),
|
||||||
|
isSelected: node.states.checked,
|
||||||
|
};
|
||||||
|
this.$emit('nodeSelected', event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,11 +80,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Watch('selectedNodeIds')
|
@Watch('selectedNodeIds')
|
||||||
public setSelectedStatus(selectedNodeIds: ReadonlyArray<string>) {
|
public setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) {
|
||||||
if (!selectedNodeIds) {
|
if (!selectedNodeIds) {
|
||||||
throw new Error('Selected nodes are undefined');
|
throw new Error('Selected nodes are undefined');
|
||||||
}
|
}
|
||||||
const newNodes = updateCheckedState(this.getLiquorTreeApi().model, selectedNodeIds);
|
const newNodes = updateNodesCheckedState(this.getLiquorTreeApi().model, selectedNodeIds);
|
||||||
this.getLiquorTreeApi().setModel(newNodes);
|
this.getLiquorTreeApi().setModel(newNodes);
|
||||||
/* Alternative:
|
/* Alternative:
|
||||||
this.getLiquorTreeApi().recurseDown((node) => {
|
this.getLiquorTreeApi().recurseDown((node) => {
|
||||||
@@ -98,27 +105,6 @@
|
|||||||
}
|
}
|
||||||
return (this.$refs.treeElement as any).tree;
|
return (this.$refs.treeElement as any).tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDefaults(): ILiquorTreeOptions {
|
|
||||||
return {
|
|
||||||
multiple: true,
|
|
||||||
checkbox: true,
|
|
||||||
checkOnSelect: true,
|
|
||||||
autoCheckChildren: true,
|
|
||||||
parentSelect: false,
|
|
||||||
keyboardNavigation: true,
|
|
||||||
deletion: (node) => !node.children || node.children.length === 0,
|
|
||||||
filter: {
|
|
||||||
matcher: (query: string, node: ILiquorTreeExistingNode) => {
|
|
||||||
if (!this.filterPredicate) {
|
|
||||||
throw new Error('Cannot filter as predicate is null');
|
|
||||||
}
|
|
||||||
return this.filterPredicate(convertExistingToNode(node));
|
|
||||||
},
|
|
||||||
emptyText: '🕵️Hmm.. Can not see one 🧐',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recurseDown(
|
function recurseDown(
|
||||||
@@ -131,27 +117,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCheckedState(
|
|
||||||
oldNodes: ReadonlyArray<ILiquorTreeExistingNode>,
|
|
||||||
selectedNodeIds: ReadonlyArray<string>): ReadonlyArray<ILiquorTreeNewNode> {
|
|
||||||
const result = new Array<ILiquorTreeNewNode>();
|
|
||||||
for (const oldNode of oldNodes) {
|
|
||||||
const newState = oldNode.states;
|
|
||||||
newState.checked = selectedNodeIds.some((id) => id === oldNode.id);
|
|
||||||
const newNode: ILiquorTreeNewNode = {
|
|
||||||
id: oldNode.id,
|
|
||||||
text: oldNode.data.text,
|
|
||||||
data: {
|
|
||||||
documentationUrls: oldNode.data.documentationUrls,
|
|
||||||
isReversible: oldNode.data.isReversible,
|
|
||||||
},
|
|
||||||
children: oldNode.children == null ? [] :
|
|
||||||
updateCheckedState(oldNode.children, selectedNodeIds),
|
|
||||||
state: newState,
|
|
||||||
};
|
|
||||||
result.push(newNode);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { Component, Prop, Watch, Vue } 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/State/Code/CodeBuilder';
|
import { CodeBuilder } from '@/application/State/Code/Generation/CodeBuilder';
|
||||||
|
import { ICodeChangedEvent } from '@/application/State/Code/Event/ICodeChangedEvent';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
const NothingChosenCode =
|
const NothingChosenCode =
|
||||||
new CodeBuilder()
|
new CodeBuilder()
|
||||||
@@ -28,19 +30,65 @@ const NothingChosenCode =
|
|||||||
@Component
|
@Component
|
||||||
export default class TheCodeArea extends StatefulVue {
|
export default class TheCodeArea extends StatefulVue {
|
||||||
public readonly editorId = 'codeEditor';
|
public readonly editorId = 'codeEditor';
|
||||||
|
|
||||||
private editor!: ace.Ace.Editor;
|
private editor!: ace.Ace.Editor;
|
||||||
|
private currentMarkerId?: number;
|
||||||
|
|
||||||
@Prop() private theme!: string;
|
@Prop() private theme!: string;
|
||||||
|
|
||||||
public async mounted() {
|
public async mounted() {
|
||||||
this.editor = initializeEditor(this.theme, this.editorId);
|
this.editor = initializeEditor(this.theme, this.editorId);
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
this.updateCode(state.code.current);
|
this.editor.setValue(state.code.current || NothingChosenCode, 1);
|
||||||
state.code.changed.on((code) => this.updateCode(code));
|
state.code.changed.on((code) => this.updateCode(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateCode(code: string) {
|
private updateCode(event: ICodeChangedEvent) {
|
||||||
this.editor.setValue(code || NothingChosenCode, 1);
|
this.removeCurrentHighlighting();
|
||||||
|
if (event.isEmpty()) {
|
||||||
|
this.editor.setValue(NothingChosenCode, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.editor.setValue(event.code, 1);
|
||||||
|
|
||||||
|
if (event.addedScripts && event.addedScripts.length) {
|
||||||
|
this.reactToChanges(event, event.addedScripts);
|
||||||
|
} else if (event.changedScripts && event.changedScripts.length) {
|
||||||
|
this.reactToChanges(event, event.changedScripts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) {
|
||||||
|
const positions = scripts
|
||||||
|
.map((script) => event.getScriptPositionInCode(script));
|
||||||
|
const start = Math.min(
|
||||||
|
...positions.map((position) => position.startLine),
|
||||||
|
);
|
||||||
|
const end = Math.max(
|
||||||
|
...positions.map((position) => position.endLine),
|
||||||
|
);
|
||||||
|
this.scrollToLine(end + 2);
|
||||||
|
this.highlight(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
private highlight(startRow: number, endRow: number) {
|
||||||
|
const AceRange = ace.require('ace/range').Range;
|
||||||
|
this.currentMarkerId = this.editor.session.addMarker(
|
||||||
|
new AceRange(startRow, 0, endRow, 0), 'code-area__highlight', 'fullLine',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollToLine(row: number) {
|
||||||
|
const column = this.editor.session.getLine(row).length;
|
||||||
|
this.editor.gotoLine(row, column, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeCurrentHighlighting() {
|
||||||
|
if (!this.currentMarkerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.editor.session.removeMarker(this.currentMarkerId);
|
||||||
|
this.currentMarkerId = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +106,16 @@ function initializeEditor(theme: string, editorId: string): ace.Ace.Editor {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss">
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
.code-area {
|
.code-area {
|
||||||
/* ----- Fill its parent div ------ */
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* height */
|
|
||||||
max-height: 1000px;
|
max-height: 1000px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
&__highlight {
|
||||||
|
background-color:$accent;
|
||||||
|
opacity: 20%;
|
||||||
|
position:absolute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default class TheCodeButtons extends StatefulVue {
|
|||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
this.hasCode = state.code.current && state.code.current.length > 0;
|
this.hasCode = state.code.current && state.code.current.length > 0;
|
||||||
state.code.changed.on((code) => {
|
state.code.changed.on((code) => {
|
||||||
this.hasCode = code && code.length > 0;
|
this.hasCode = code && code.code.length > 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
import { CategoryStub } from './../../../stubs/CategoryStub';
|
import { CategoryStub } from './../../../stubs/CategoryStub';
|
||||||
import { ScriptStub } from './../../../stubs/ScriptStub';
|
import { ScriptStub } from './../../../stubs/ScriptStub';
|
||||||
import { ApplicationStub } from './../../../stubs/ApplicationStub';
|
import { ApplicationStub } from './../../../stubs/ApplicationStub';
|
||||||
import { UserSelection } from '@/application/State/Selection/UserSelection';
|
import { UserSelection } from '@/application/State/Selection/UserSelection';
|
||||||
import { ApplicationCode } from '@/application/State/Code/ApplicationCode';
|
import { ApplicationCode } from '@/application/State/Code/ApplicationCode';
|
||||||
import 'mocha';
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { ICodeChangedEvent } from '@/application/State/Code/Event/ICodeChangedEvent';
|
||||||
|
import { IUserScriptGenerator } from '@/application/State/Code/Generation/IUserScriptGenerator';
|
||||||
|
import { CodePosition } from '@/application/State/Code/Position/CodePosition';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
describe('ApplicationCode', () => {
|
describe('ApplicationCode', () => {
|
||||||
describe('ctor', () => {
|
describe('ctor', () => {
|
||||||
@@ -31,35 +35,76 @@ describe('ApplicationCode', () => {
|
|||||||
expect(actual).to.have.length.greaterThan(0).and.include(version);
|
expect(actual).to.have.length.greaterThan(0).and.include(version);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('user selection changes', () => {
|
describe('changed event', () => {
|
||||||
it('empty when selection is empty', () => {
|
describe('code', () => {
|
||||||
// arrange
|
it('empty when nothing is selected', () => {
|
||||||
let signaled: string;
|
// arrange
|
||||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
let signaled: ICodeChangedEvent;
|
||||||
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
|
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||||
const selection = new UserSelection(app, scripts);
|
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
|
||||||
const sut = new ApplicationCode(selection, 'version');
|
const selection = new UserSelection(app, scripts);
|
||||||
sut.changed.on((code) => signaled = code);
|
const sut = new ApplicationCode(selection, 'version');
|
||||||
// act
|
sut.changed.on((code) => signaled = code);
|
||||||
selection.changed.notify([]);
|
// act
|
||||||
// assert
|
selection.changed.notify([]);
|
||||||
expect(signaled).to.have.lengthOf(0);
|
// assert
|
||||||
expect(signaled).to.equal(sut.current);
|
expect(signaled.code).to.have.lengthOf(0);
|
||||||
|
expect(signaled.code).to.equal(sut.current);
|
||||||
|
});
|
||||||
|
it('has code when some are selected', () => {
|
||||||
|
// arrange
|
||||||
|
let signaled: ICodeChangedEvent;
|
||||||
|
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||||
|
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
|
||||||
|
const selection = new UserSelection(app, scripts);
|
||||||
|
const version = 'version-string';
|
||||||
|
const sut = new ApplicationCode(selection, version);
|
||||||
|
sut.changed.on((code) => signaled = code);
|
||||||
|
// act
|
||||||
|
selection.changed.notify(scripts.map((s) => new SelectedScript(s, false)));
|
||||||
|
// assert
|
||||||
|
expect(signaled.code).to.have.length.greaterThan(0).and.include(version);
|
||||||
|
expect(signaled.code).to.equal(sut.current);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('has code when selection is not empty', () => {
|
it('sets positions from the generator', () => {
|
||||||
// arrange
|
// arrange
|
||||||
let signaled: string;
|
let signaled: ICodeChangedEvent;
|
||||||
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
|
||||||
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
|
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
|
||||||
const selection = new UserSelection(app, scripts);
|
const selection = new UserSelection(app, scripts);
|
||||||
const version = 'version-string';
|
const expectedVersion = 'version-string';
|
||||||
const sut = new ApplicationCode(selection, version);
|
const scriptsToSelect = scripts.map((s) => new SelectedScript(s, false));
|
||||||
|
const totalLines = 20;
|
||||||
|
const expected = new Map<SelectedScript, ICodePosition>(
|
||||||
|
[
|
||||||
|
[ scriptsToSelect[0], new CodePosition(0, totalLines / 2)],
|
||||||
|
[ scriptsToSelect[1], new CodePosition(totalLines / 2, totalLines)],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const generatorMock: IUserScriptGenerator = {
|
||||||
|
buildCode: (selectedScripts, version) => {
|
||||||
|
if (version !== expectedVersion) {
|
||||||
|
throw new Error('Unexpected version');
|
||||||
|
}
|
||||||
|
if (JSON.stringify(selectedScripts) !== JSON.stringify(scriptsToSelect)) {
|
||||||
|
throw new Error('Unexpected scripts');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: '\nREM LINE'.repeat(totalLines),
|
||||||
|
scriptPositions: expected,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const sut = new ApplicationCode(selection, expectedVersion, generatorMock);
|
||||||
sut.changed.on((code) => signaled = code);
|
sut.changed.on((code) => signaled = code);
|
||||||
// act
|
// act
|
||||||
selection.changed.notify(scripts.map((s) => new SelectedScript(s, false)));
|
selection.changed.notify(scriptsToSelect);
|
||||||
// assert
|
// assert
|
||||||
expect(signaled).to.have.length.greaterThan(0).and.include(version);
|
expect(signaled.getScriptPositionInCode(scripts[0]))
|
||||||
expect(signaled).to.equal(sut.current);
|
.to.deep.equal(expected.get(scriptsToSelect[0]));
|
||||||
|
expect(signaled.getScriptPositionInCode(scripts[1]))
|
||||||
|
.to.deep.equal(expected.get(scriptsToSelect[1]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
147
tests/unit/application/State/Code/Event/CodeChangedEvent.spec.ts
Normal file
147
tests/unit/application/State/Code/Event/CodeChangedEvent.spec.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { CodeChangedEvent } from '@/application/State/Code/Event/CodeChangedEvent';
|
||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
import { CodePosition } from '@/application/State/Code/Position/CodePosition';
|
||||||
|
import { SelectedScriptStub } from '../../../../stubs/SelectedScriptStub';
|
||||||
|
import { ScriptStub } from '../../../../stubs/ScriptStub';
|
||||||
|
|
||||||
|
describe('CodeChangedEvent', () => {
|
||||||
|
describe('ctor', () => {
|
||||||
|
describe('position validation', () => {
|
||||||
|
it('throws when code position is out of range', () => {
|
||||||
|
const act = () => new CodeChangedEvent(
|
||||||
|
'singleline code', [], new Map<SelectedScript, ICodePosition>([
|
||||||
|
[ new SelectedScriptStub('1'), new CodePosition(0, 2) ],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(act).to.throw();
|
||||||
|
});
|
||||||
|
it('does not throw with valid code position', () => {
|
||||||
|
const act = () => new CodeChangedEvent(
|
||||||
|
'singleline code', [], new Map<SelectedScript, ICodePosition>([
|
||||||
|
[ new SelectedScriptStub('1'), new CodePosition(0, 1) ],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(act).to.not.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('code returns expected', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = 'code';
|
||||||
|
// act
|
||||||
|
const sut = new CodeChangedEvent(
|
||||||
|
expected, [], new Map<SelectedScript, ICodePosition>(),
|
||||||
|
);
|
||||||
|
const actual = sut.code;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(expected);
|
||||||
|
});
|
||||||
|
describe('addedScripts', () => {
|
||||||
|
it('returns new scripts when scripts are added', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = [ new ScriptStub('3'), new ScriptStub('4') ];
|
||||||
|
const initialScripts = [ new SelectedScriptStub('1'), new SelectedScriptStub('2') ];
|
||||||
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||||
|
[initialScripts[0], new CodePosition(0, 1) ],
|
||||||
|
[initialScripts[1], new CodePosition(0, 1) ],
|
||||||
|
[new SelectedScript(expected[0], false), new CodePosition(0, 1) ],
|
||||||
|
[new SelectedScript(expected[1], false), new CodePosition(0, 1) ],
|
||||||
|
]);
|
||||||
|
// act
|
||||||
|
const sut = new CodeChangedEvent(
|
||||||
|
'code', initialScripts, newScripts,
|
||||||
|
);
|
||||||
|
const actual = sut.addedScripts;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.have.lengthOf(2);
|
||||||
|
expect(actual[0]).to.deep.equal(expected[0]);
|
||||||
|
expect(actual[1]).to.deep.equal(expected[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('removedScripts', () => {
|
||||||
|
it('returns removed scripts when script are removed', () => {
|
||||||
|
// arrange
|
||||||
|
const existingScripts = [ new SelectedScriptStub('0'), new SelectedScriptStub('1') ];
|
||||||
|
const removedScripts = [ new SelectedScriptStub('2') ];
|
||||||
|
const initialScripts = [ ...existingScripts, ...removedScripts ];
|
||||||
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||||
|
[initialScripts[0], new CodePosition(0, 1) ],
|
||||||
|
[initialScripts[1], new CodePosition(0, 1) ],
|
||||||
|
]);
|
||||||
|
// act
|
||||||
|
const sut = new CodeChangedEvent(
|
||||||
|
'code', initialScripts, newScripts,
|
||||||
|
);
|
||||||
|
const actual = sut.removedScripts;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.have.lengthOf(removedScripts.length);
|
||||||
|
expect(actual[0]).to.deep.equal(removedScripts[0].script);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('changedScripts', () => {
|
||||||
|
it('returns changed scripts when scripts are changed', () => {
|
||||||
|
// arrange
|
||||||
|
const initialScripts = [ new SelectedScriptStub('1', false), new SelectedScriptStub('2', false) ];
|
||||||
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||||
|
[new SelectedScriptStub('1', true), new CodePosition(0, 1) ],
|
||||||
|
[new SelectedScriptStub('2', false), new CodePosition(0, 1) ],
|
||||||
|
]);
|
||||||
|
// act
|
||||||
|
const sut = new CodeChangedEvent(
|
||||||
|
'code', initialScripts, newScripts,
|
||||||
|
);
|
||||||
|
const actual = sut.changedScripts;
|
||||||
|
// assert
|
||||||
|
expect(actual).to.have.lengthOf(1);
|
||||||
|
expect(actual[0]).to.deep.equal(initialScripts[0].script);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('isEmpty', () => {
|
||||||
|
it('returns true when empty', () => {
|
||||||
|
// arrange
|
||||||
|
const newScripts = new Map<SelectedScript, ICodePosition>();
|
||||||
|
const oldScripts = [ new SelectedScriptStub('1', false) ];
|
||||||
|
const sut = new CodeChangedEvent(
|
||||||
|
'code', oldScripts, newScripts,
|
||||||
|
);
|
||||||
|
// act
|
||||||
|
const actual = sut.isEmpty();
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(true);
|
||||||
|
});
|
||||||
|
it('returns false when not empty', () => {
|
||||||
|
// arrange
|
||||||
|
const oldScripts = [ new SelectedScriptStub('1') ];
|
||||||
|
const newScripts = new Map<SelectedScript, ICodePosition>( [
|
||||||
|
[oldScripts[0], new CodePosition(0, 1) ],
|
||||||
|
]);
|
||||||
|
const sut = new CodeChangedEvent(
|
||||||
|
'code', oldScripts, newScripts,
|
||||||
|
);
|
||||||
|
// act
|
||||||
|
const actual = sut.isEmpty();
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('getScriptPositionInCode', () => {
|
||||||
|
it('returns expected position for existing script', () => {
|
||||||
|
// arrange
|
||||||
|
const script = new ScriptStub('1');
|
||||||
|
const expected = new CodePosition(0, 1);
|
||||||
|
const newScripts = new Map<SelectedScript, ICodePosition>( [
|
||||||
|
[new SelectedScript(script, false), expected ],
|
||||||
|
]);
|
||||||
|
const sut = new CodeChangedEvent(
|
||||||
|
'code', [], newScripts,
|
||||||
|
);
|
||||||
|
// act
|
||||||
|
const actual = sut.getScriptPositionInCode(script);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
112
tests/unit/application/State/Code/Generation/CodeBuilder.spec.ts
Normal file
112
tests/unit/application/State/Code/Generation/CodeBuilder.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { CodeBuilder } from '@/application/State/Code/Generation/CodeBuilder';
|
||||||
|
|
||||||
|
describe('CodeBuilder', () => {
|
||||||
|
describe('appendLine', () => {
|
||||||
|
it('when empty appends empty line', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new CodeBuilder();
|
||||||
|
// act
|
||||||
|
sut.appendLine().appendLine().appendLine();
|
||||||
|
// assert
|
||||||
|
expect(sut.toString()).to.equal('\n\n');
|
||||||
|
});
|
||||||
|
it('when not empty append string in new line', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new CodeBuilder();
|
||||||
|
const expected = 'str';
|
||||||
|
// act
|
||||||
|
sut.appendLine()
|
||||||
|
.appendLine(expected);
|
||||||
|
// assert
|
||||||
|
const result = sut.toString();
|
||||||
|
const lines = getLines(result);
|
||||||
|
expect(lines[1]).to.equal('str');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('appendFunction', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new CodeBuilder();
|
||||||
|
const functionName = 'function';
|
||||||
|
const code = 'code';
|
||||||
|
// act
|
||||||
|
sut.appendFunction(functionName, code);
|
||||||
|
// assert
|
||||||
|
const result = sut.toString();
|
||||||
|
expect(result).to.include(functionName);
|
||||||
|
expect(result).to.include(code);
|
||||||
|
});
|
||||||
|
it('appendTrailingHyphensCommentLine', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new CodeBuilder();
|
||||||
|
const totalHypens = 5;
|
||||||
|
const expected = `:: ${'-'.repeat(totalHypens)}`;
|
||||||
|
// act
|
||||||
|
sut.appendTrailingHyphensCommentLine(totalHypens);
|
||||||
|
// assert
|
||||||
|
const result = sut.toString();
|
||||||
|
const lines = getLines(result);
|
||||||
|
expect(lines[0]).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('appendCommentLine', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new CodeBuilder();
|
||||||
|
const comment = 'comment';
|
||||||
|
const expected = ':: comment';
|
||||||
|
// act
|
||||||
|
sut.appendCommentLine(comment);
|
||||||
|
// assert
|
||||||
|
const result = sut.toString();
|
||||||
|
const lines = getLines(result);
|
||||||
|
expect(lines[0]).to.equal(expected);
|
||||||
|
});
|
||||||
|
it('appendCommentLineWithHyphensAround', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new CodeBuilder();
|
||||||
|
const sectionName = 'section';
|
||||||
|
const totalHypens = sectionName.length + 3 * 2;
|
||||||
|
const expected = ':: ---section---';
|
||||||
|
sut.appendCommentLineWithHyphensAround(sectionName, totalHypens);
|
||||||
|
// assert
|
||||||
|
const result = sut.toString();
|
||||||
|
const lines = getLines(result);
|
||||||
|
expect(lines[1]).to.equal(expected);
|
||||||
|
});
|
||||||
|
describe('currentLine', () => {
|
||||||
|
it('no lines returns zero', () => {
|
||||||
|
// arrange & act
|
||||||
|
const sut = new CodeBuilder();
|
||||||
|
// assert
|
||||||
|
expect(sut.currentLine).to.equal(0);
|
||||||
|
});
|
||||||
|
it('single line returns one', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new CodeBuilder();
|
||||||
|
// act
|
||||||
|
sut.appendLine();
|
||||||
|
// assert
|
||||||
|
expect(sut.currentLine).to.equal(1);
|
||||||
|
});
|
||||||
|
it('multiple lines returns as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new CodeBuilder();
|
||||||
|
// act
|
||||||
|
sut.appendLine('1').appendCommentLine('2').appendLine();
|
||||||
|
// assert
|
||||||
|
expect(sut.currentLine).to.equal(3);
|
||||||
|
});
|
||||||
|
it('multiple lines in code', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new CodeBuilder();
|
||||||
|
// act
|
||||||
|
sut.appendLine('hello\ncode-here\nwith-3-lines');
|
||||||
|
// assert
|
||||||
|
expect(sut.currentLine).to.equal(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getLines(text: string): string[] {
|
||||||
|
return text.split(/\r\n|\r|\n/);
|
||||||
|
}
|
||||||
@@ -1,33 +1,34 @@
|
|||||||
import { ScriptStub } from './../../../stubs/ScriptStub';
|
import { ScriptStub } from '../../../../stubs/ScriptStub';
|
||||||
import { UserScriptGenerator, adminRightsScript } from '@/application/State/Code/UserScriptGenerator';
|
import { UserScriptGenerator, adminRightsScript } from '@/application/State/Code/Generation/UserScriptGenerator';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { SelectedScriptStub } from '../../../../stubs/SelectedScriptStub';
|
||||||
|
|
||||||
describe('UserScriptGenerator', () => {
|
describe('UserScriptGenerator', () => {
|
||||||
it('adds version', () => {
|
it('adds version', () => {
|
||||||
const sut = new UserScriptGenerator();
|
|
||||||
// arrange
|
// arrange
|
||||||
|
const sut = new UserScriptGenerator();
|
||||||
const version = '1.5.0';
|
const version = '1.5.0';
|
||||||
const selectedScripts = [ new SelectedScript(new ScriptStub('id'), false)];
|
const selectedScripts = [ new SelectedScript(new ScriptStub('id'), false)];
|
||||||
// act
|
// act
|
||||||
const actual = sut.buildCode(selectedScripts, version);
|
const actual = sut.buildCode(selectedScripts, version);
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.include(version);
|
expect(actual.code).to.include(version);
|
||||||
});
|
});
|
||||||
it('adds admin rights function', () => {
|
it('adds admin rights function', () => {
|
||||||
const sut = new UserScriptGenerator();
|
|
||||||
// arrange
|
// arrange
|
||||||
|
const sut = new UserScriptGenerator();
|
||||||
const selectedScripts = [ new SelectedScript(new ScriptStub('id'), false)];
|
const selectedScripts = [ new SelectedScript(new ScriptStub('id'), false)];
|
||||||
// act
|
// act
|
||||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.include(adminRightsScript.code);
|
expect(actual.code).to.include(adminRightsScript.code);
|
||||||
expect(actual).to.include(adminRightsScript.name);
|
expect(actual.code).to.include(adminRightsScript.name);
|
||||||
});
|
});
|
||||||
it('appends revert script', () => {
|
it('appends revert script', () => {
|
||||||
const sut = new UserScriptGenerator();
|
|
||||||
// arrange
|
// arrange
|
||||||
|
const sut = new UserScriptGenerator();
|
||||||
const scriptName = 'test non-revert script';
|
const scriptName = 'test non-revert script';
|
||||||
const scriptCode = 'REM nop';
|
const scriptCode = 'REM nop';
|
||||||
const script = new ScriptStub('id').withName(scriptName).withRevertCode(scriptCode);
|
const script = new ScriptStub('id').withName(scriptName).withRevertCode(scriptCode);
|
||||||
@@ -35,8 +36,8 @@ describe('UserScriptGenerator', () => {
|
|||||||
// act
|
// act
|
||||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.include(`${scriptName} (revert)`);
|
expect(actual.code).to.include(`${scriptName} (revert)`);
|
||||||
expect(actual).to.include(scriptCode);
|
expect(actual.code).to.include(scriptCode);
|
||||||
});
|
});
|
||||||
it('appends non-revert script', () => {
|
it('appends non-revert script', () => {
|
||||||
const sut = new UserScriptGenerator();
|
const sut = new UserScriptGenerator();
|
||||||
@@ -48,7 +49,46 @@ describe('UserScriptGenerator', () => {
|
|||||||
// act
|
// act
|
||||||
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.include(scriptName);
|
expect(actual.code).to.include(scriptName);
|
||||||
expect(actual).to.include(scriptCode);
|
expect(actual.code).to.include(scriptCode);
|
||||||
|
});
|
||||||
|
describe('scriptPositions', () => {
|
||||||
|
it('single script', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new UserScriptGenerator();
|
||||||
|
const scriptName = 'test non-revert script';
|
||||||
|
const scriptCode = 'REM nop\nREM nop2';
|
||||||
|
const script = new ScriptStub('id').withName(scriptName).withCode(scriptCode);
|
||||||
|
const selectedScripts = [ new SelectedScript(script, false)];
|
||||||
|
// act
|
||||||
|
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||||
|
// assert
|
||||||
|
expect(actual.scriptPositions.size).to.equal(1);
|
||||||
|
const position = actual.scriptPositions.get(selectedScripts[0]);
|
||||||
|
expect(position.endLine).to.be.greaterThan(position.startLine + 2);
|
||||||
|
});
|
||||||
|
it('multiple scripts', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new UserScriptGenerator();
|
||||||
|
const selectedScripts = [ new SelectedScriptStub('1'), new SelectedScriptStub('2') ];
|
||||||
|
// act
|
||||||
|
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||||
|
// assert
|
||||||
|
const firstPosition = actual.scriptPositions.get(selectedScripts[0]);
|
||||||
|
const secondPosition = actual.scriptPositions.get(selectedScripts[1]);
|
||||||
|
expect(actual.scriptPositions.size).to.equal(2);
|
||||||
|
expect(firstPosition.endLine).to.be.greaterThan(firstPosition.startLine + 1);
|
||||||
|
expect(secondPosition.startLine).to.be.greaterThan(firstPosition.endLine);
|
||||||
|
expect(secondPosition.endLine).to.be.greaterThan(secondPosition.startLine + 1);
|
||||||
|
});
|
||||||
|
it('no script', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new UserScriptGenerator();
|
||||||
|
const selectedScripts = [ ];
|
||||||
|
// act
|
||||||
|
const actual = sut.buildCode(selectedScripts, 'non-important-version');
|
||||||
|
// assert
|
||||||
|
expect(actual.scriptPositions.size).to.equal(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { CodePosition } from '@/application/State/Code/Position/CodePosition';
|
||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
describe('CodePosition', () => {
|
||||||
|
describe('ctor', () => {
|
||||||
|
it('creates with valid parameters', () => {
|
||||||
|
// arrange
|
||||||
|
const startPosition = 0;
|
||||||
|
const endPosition = 5;
|
||||||
|
// act
|
||||||
|
const sut = new CodePosition(startPosition, endPosition);
|
||||||
|
// assert
|
||||||
|
expect(sut.startLine).to.equal(startPosition);
|
||||||
|
expect(sut.endLine).to.equal(endPosition);
|
||||||
|
});
|
||||||
|
it('throws with negative start position', () => {
|
||||||
|
// arrange
|
||||||
|
const startPosition = -1;
|
||||||
|
const endPosition = 5;
|
||||||
|
// act
|
||||||
|
const getSut = () => new CodePosition(startPosition, endPosition);
|
||||||
|
// assert
|
||||||
|
expect(getSut).to.throw('Code cannot start in a negative line');
|
||||||
|
});
|
||||||
|
it('throws with negative end position', () => {
|
||||||
|
// arrange
|
||||||
|
const startPosition = 1;
|
||||||
|
const endPosition = -5;
|
||||||
|
// act
|
||||||
|
const getSut = () => new CodePosition(startPosition, endPosition);
|
||||||
|
// assert
|
||||||
|
expect(getSut).to.throw('Code cannot end in a negative line');
|
||||||
|
});
|
||||||
|
it('throws when start and end position is same', () => {
|
||||||
|
// arrange
|
||||||
|
const startPosition = 0;
|
||||||
|
const endPosition = 0;
|
||||||
|
// act
|
||||||
|
const getSut = () => new CodePosition(startPosition, endPosition);
|
||||||
|
// assert
|
||||||
|
expect(getSut).to.throw('Empty code');
|
||||||
|
});
|
||||||
|
it('throws when ends before start', () => {
|
||||||
|
// arrange
|
||||||
|
const startPosition = 3;
|
||||||
|
const endPosition = 2;
|
||||||
|
// act
|
||||||
|
const getSut = () => new CodePosition(startPosition, endPosition);
|
||||||
|
// assert
|
||||||
|
expect(getSut).to.throw('End line cannot be less than start line');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -93,4 +93,127 @@ describe('UserSelection', () => {
|
|||||||
expect(events[0]).to.deep.equal(expected);
|
expect(events[0]).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('removeAllInCategory', () => {
|
||||||
|
it('does nothing when nothing exists', () => {
|
||||||
|
// arrange
|
||||||
|
const events: Array<readonly SelectedScript[]> = [];
|
||||||
|
const categoryId = 1;
|
||||||
|
const app = new ApplicationStub()
|
||||||
|
.withAction(new CategoryStub(categoryId)
|
||||||
|
.withScripts(new ScriptStub('s1'), new ScriptStub('s2')));
|
||||||
|
const sut = new UserSelection(app, []);
|
||||||
|
sut.changed.on((s) => events.push(s));
|
||||||
|
// act
|
||||||
|
sut.removeAllInCategory(categoryId);
|
||||||
|
// assert
|
||||||
|
expect(events).to.have.lengthOf(0);
|
||||||
|
expect(sut.selectedScripts).to.have.lengthOf(0);
|
||||||
|
});
|
||||||
|
it('removes all when all exists', () => {
|
||||||
|
// arrange
|
||||||
|
const categoryId = 1;
|
||||||
|
const scripts = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||||
|
const app = new ApplicationStub()
|
||||||
|
.withAction(new CategoryStub(categoryId)
|
||||||
|
.withScripts(...scripts));
|
||||||
|
const sut = new UserSelection(app, scripts);
|
||||||
|
// act
|
||||||
|
sut.removeAllInCategory(categoryId);
|
||||||
|
// assert
|
||||||
|
expect(sut.totalSelected).to.equal(0);
|
||||||
|
expect(sut.selectedScripts.length).to.equal(0);
|
||||||
|
});
|
||||||
|
it('removes existing some exists', () => {
|
||||||
|
// arrange
|
||||||
|
const categoryId = 1;
|
||||||
|
const existing = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||||
|
const notExisting = [new ScriptStub('s3'), new ScriptStub('s4')];
|
||||||
|
const app = new ApplicationStub()
|
||||||
|
.withAction(new CategoryStub(categoryId)
|
||||||
|
.withScripts(...existing, ...notExisting));
|
||||||
|
const sut = new UserSelection(app, existing);
|
||||||
|
// act
|
||||||
|
sut.removeAllInCategory(categoryId);
|
||||||
|
// assert
|
||||||
|
expect(sut.totalSelected).to.equal(0);
|
||||||
|
expect(sut.selectedScripts.length).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('addAllInCategory', () => {
|
||||||
|
it('does nothing when all already exists', () => {
|
||||||
|
// arrange
|
||||||
|
const events: Array<readonly SelectedScript[]> = [];
|
||||||
|
const categoryId = 1;
|
||||||
|
const scripts = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||||
|
const app = new ApplicationStub()
|
||||||
|
.withAction(new CategoryStub(categoryId)
|
||||||
|
.withScripts(...scripts));
|
||||||
|
const sut = new UserSelection(app, scripts);
|
||||||
|
sut.changed.on((s) => events.push(s));
|
||||||
|
// act
|
||||||
|
sut.addAllInCategory(categoryId);
|
||||||
|
// assert
|
||||||
|
expect(events).to.have.lengthOf(0);
|
||||||
|
expect(sut.selectedScripts.map((script) => script.id))
|
||||||
|
.to.have.deep.members(scripts.map((script) => script.id));
|
||||||
|
});
|
||||||
|
it('adds all when nothing exists', () => {
|
||||||
|
// arrange
|
||||||
|
const categoryId = 1;
|
||||||
|
const expected = [new ScriptStub('s1'), new ScriptStub('s2')];
|
||||||
|
const app = new ApplicationStub()
|
||||||
|
.withAction(new CategoryStub(categoryId)
|
||||||
|
.withScripts(...expected));
|
||||||
|
const sut = new UserSelection(app, []);
|
||||||
|
// act
|
||||||
|
sut.addAllInCategory(categoryId);
|
||||||
|
// assert
|
||||||
|
expect(sut.selectedScripts.map((script) => script.id))
|
||||||
|
.to.have.deep.members(expected.map((script) => script.id));
|
||||||
|
});
|
||||||
|
it('adds not existing some exists', () => {
|
||||||
|
// arrange
|
||||||
|
const categoryId = 1;
|
||||||
|
const notExisting = [ new ScriptStub('notExisting1'), new ScriptStub('notExisting2') ];
|
||||||
|
const existing = [ new ScriptStub('existing1'), new ScriptStub('existing2') ];
|
||||||
|
const allScripts = [ ...existing, ...notExisting ];
|
||||||
|
const app = new ApplicationStub()
|
||||||
|
.withAction(new CategoryStub(categoryId)
|
||||||
|
.withScripts(...allScripts));
|
||||||
|
const sut = new UserSelection(app, existing);
|
||||||
|
// act
|
||||||
|
sut.addAllInCategory(categoryId);
|
||||||
|
// assert
|
||||||
|
expect(sut.selectedScripts.map((script) => script.id))
|
||||||
|
.to.have.deep.members(allScripts.map((script) => script.id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('isSelected', () => {
|
||||||
|
it('returns false when not selected', () => {
|
||||||
|
// arrange
|
||||||
|
const selectedScript = new ScriptStub('selected');
|
||||||
|
const notSelectedScript = new ScriptStub('not selected');
|
||||||
|
const app = new ApplicationStub()
|
||||||
|
.withAction(new CategoryStub(1)
|
||||||
|
.withScripts(selectedScript, notSelectedScript));
|
||||||
|
const sut = new UserSelection(app, [ selectedScript ]);
|
||||||
|
// act
|
||||||
|
const actual = sut.isSelected(notSelectedScript.id);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(false);
|
||||||
|
});
|
||||||
|
it('returns true when selected', () => {
|
||||||
|
// arrange
|
||||||
|
const selectedScript = new ScriptStub('selected');
|
||||||
|
const notSelectedScript = new ScriptStub('not selected');
|
||||||
|
const app = new ApplicationStub()
|
||||||
|
.withAction(new CategoryStub(1)
|
||||||
|
.withScripts(selectedScript, notSelectedScript));
|
||||||
|
const sut = new UserSelection(app, [ selectedScript ]);
|
||||||
|
// act
|
||||||
|
const actual = sut.isSelected(selectedScript.id);
|
||||||
|
// assert
|
||||||
|
expect(actual).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
89
tests/unit/domain/Category.spec.ts
Normal file
89
tests/unit/domain/Category.spec.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { Category } from '@/domain/Category';
|
||||||
|
import { CategoryStub } from '../stubs/CategoryStub';
|
||||||
|
import { ScriptStub } from '../stubs/ScriptStub';
|
||||||
|
|
||||||
|
describe('Category', () => {
|
||||||
|
describe('ctor', () => {
|
||||||
|
it('throws when name is empty', () => {
|
||||||
|
const expectedError = 'undefined or empty name';
|
||||||
|
const construct = () => new Category(5, '', [], [new CategoryStub(5)], []);
|
||||||
|
expect(construct).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
it('throws when has no children', () => {
|
||||||
|
const expectedError = 'A category must have at least one sub-category or script';
|
||||||
|
const construct = () => new Category(5, 'category', [], [], []);
|
||||||
|
expect(construct).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('getAllScriptsRecursively', () => {
|
||||||
|
it('gets child scripts', () => {
|
||||||
|
// arrange
|
||||||
|
const expected = [ new ScriptStub('1'), new ScriptStub('2') ];
|
||||||
|
const sut = new Category(0, 'category', [], [], expected);
|
||||||
|
// act
|
||||||
|
const actual = sut.getAllScriptsRecursively();
|
||||||
|
// assert
|
||||||
|
expect(actual).to.have.deep.members(expected);
|
||||||
|
});
|
||||||
|
it('gets child categories', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScriptIds = ['1', '2', '3', '4'];
|
||||||
|
const categories = [
|
||||||
|
new CategoryStub(31).withScriptIds('1', '2'),
|
||||||
|
new CategoryStub(32).withScriptIds('3', '4'),
|
||||||
|
];
|
||||||
|
const sut = new Category(0, 'category', [], categories, []);
|
||||||
|
// act
|
||||||
|
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
||||||
|
// assert
|
||||||
|
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||||
|
|
||||||
|
});
|
||||||
|
it('gets child scripts and categories', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScriptIds = ['1', '2', '3', '4', '5' , '6'];
|
||||||
|
const categories = [
|
||||||
|
new CategoryStub(31).withScriptIds('1', '2'),
|
||||||
|
new CategoryStub(32).withScriptIds('3', '4'),
|
||||||
|
];
|
||||||
|
const scripts = [ new ScriptStub('5'), new ScriptStub('6') ];
|
||||||
|
const sut = new Category(0, 'category', [], categories, scripts);
|
||||||
|
// act
|
||||||
|
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
||||||
|
// assert
|
||||||
|
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||||
|
|
||||||
|
});
|
||||||
|
it('gets child categories recursively', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScriptIds = ['1', '2', '3', '4', '5', '6'];
|
||||||
|
const categories = [
|
||||||
|
new CategoryStub(31)
|
||||||
|
.withScriptIds('1', '2')
|
||||||
|
.withCategory(
|
||||||
|
new CategoryStub(32)
|
||||||
|
.withScriptIds('3', '4'),
|
||||||
|
),
|
||||||
|
new CategoryStub(33)
|
||||||
|
.withCategories(
|
||||||
|
new CategoryStub(34)
|
||||||
|
.withScriptIds('5')
|
||||||
|
.withCategory(
|
||||||
|
new CategoryStub(35)
|
||||||
|
.withCategory(
|
||||||
|
new CategoryStub(35).withScriptIds('6'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
// assert
|
||||||
|
const sut = new Category(0, 'category', [], categories, []);
|
||||||
|
// act
|
||||||
|
const actualIds = sut.getAllScriptsRecursively().map((s) => s.id);
|
||||||
|
// assert
|
||||||
|
expect(actualIds).to.have.deep.members(expectedScriptIds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,10 +13,11 @@ export class ApplicationStub implements IApplication {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
public findCategory(categoryId: number): ICategory {
|
public findCategory(categoryId: number): ICategory {
|
||||||
throw new Error('Method not implemented.');
|
return this.getAllCategories().find(
|
||||||
|
(category) => category.id === categoryId);
|
||||||
}
|
}
|
||||||
public getRecommendedScripts(): readonly IScript[] {
|
public getRecommendedScripts(): readonly IScript[] {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented: getRecommendedScripts');
|
||||||
}
|
}
|
||||||
public findScript(scriptId: string): IScript {
|
public findScript(scriptId: string): IScript {
|
||||||
return this.getAllScripts().find((script) => scriptId === script.id);
|
return this.getAllScripts().find((script) => scriptId === script.id);
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ export class CategoryStub extends BaseEntity<number> implements ICategory {
|
|||||||
constructor(id: number) {
|
constructor(id: number) {
|
||||||
super(id);
|
super(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAllScriptsRecursively(): readonly IScript[] {
|
||||||
|
return [
|
||||||
|
...this.scripts,
|
||||||
|
...this.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public withScriptIds(...scriptIds: string[]): CategoryStub {
|
public withScriptIds(...scriptIds: string[]): CategoryStub {
|
||||||
for (const scriptId of scriptIds) {
|
for (const scriptId of scriptIds) {
|
||||||
this.withScript(new ScriptStub(scriptId));
|
this.withScript(new ScriptStub(scriptId));
|
||||||
|
|||||||
8
tests/unit/stubs/SelectedScriptStub.ts
Normal file
8
tests/unit/stubs/SelectedScriptStub.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { ScriptStub } from './ScriptStub';
|
||||||
|
|
||||||
|
export class SelectedScriptStub extends SelectedScript {
|
||||||
|
constructor(id: string, revert = false) {
|
||||||
|
super(new ScriptStub(id), revert);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user