Refactor code to comply with ESLint rules

Major refactoring using ESLint with rules from AirBnb and Vue.

Enable most of the ESLint rules and do necessary linting in the code.
Also add more information for rules that are disabled to describe what
they are and why they are disabled.

Allow logging (`console.log`) in test files, and in development mode
(e.g. when working with `npm run serve`), but disable it when
environment is production (as pre-configured by Vue). Also add flag
(`--mode production`) in `lint:eslint` command so production linting is
executed earlier in lifecycle.

Disable rules that requires a separate work. Such as ESLint rules that
are broken in TypeScript: no-useless-constructor (eslint/eslint#14118)
and no-shadow (eslint/eslint#13014).
This commit is contained in:
undergroundwires
2022-01-02 18:20:14 +01:00
parent 96265b75de
commit 5b1fbe1e2f
341 changed files with 16126 additions and 15101 deletions

View File

@@ -4,29 +4,37 @@ import { CategoryCollectionStateStub } from '@tests/unit/stubs/CategoryCollectio
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
export class SelectionStateTestScenario {
public readonly all: readonly SelectedScript[];
public readonly allStandard: readonly SelectedScript[];
public readonly someStandard: readonly SelectedScript[];
public readonly someStrict: readonly SelectedScript[];
public readonly allStrict: readonly SelectedScript[];
public readonly someUnrecommended: readonly SelectedScript[];
public readonly allUnrecommended: readonly SelectedScript[];
constructor() {
this.someStandard = createSelectedScripts(RecommendationLevel.Standard, 'standard-some-1', 'standard-some-2');
this.allStandard = [...this.someStandard, ...createSelectedScripts(RecommendationLevel.Standard, 'standard-all-1', 'standard-all-2')];
this.someStrict = createSelectedScripts(RecommendationLevel.Strict, 'strict-some-1', 'strict-some-2');
this.allStrict = [...this.someStrict, ...createSelectedScripts(RecommendationLevel.Strict, 'strict-all-1', 'strict-all-2')];
this.someUnrecommended = createSelectedScripts(undefined, 'unrecommended-some-1', 'unrecommended-some-2');
this.allUnrecommended = [...this.someUnrecommended, ...createSelectedScripts(undefined, 'unrecommended-all-1', 'unrecommended-all-2')];
this.all = [...this.allStandard, ...this.allStrict, ...this.allUnrecommended];
}
public generateState(selectedScripts: readonly SelectedScript[]) {
const allScripts = this.all.map((s) => s.script);
return new CategoryCollectionStateStub(allScripts)
.withSelectedScripts(selectedScripts);
}
public readonly all: readonly SelectedScript[];
public readonly allStandard: readonly SelectedScript[];
public readonly someStandard: readonly SelectedScript[];
public readonly someStrict: readonly SelectedScript[];
public readonly allStrict: readonly SelectedScript[];
public readonly someUnrecommended: readonly SelectedScript[];
public readonly allUnrecommended: readonly SelectedScript[];
constructor() {
this.someStandard = createSelectedScripts(RecommendationLevel.Standard, 'standard-some-1', 'standard-some-2');
this.allStandard = [...this.someStandard, ...createSelectedScripts(RecommendationLevel.Standard, 'standard-all-1', 'standard-all-2')];
this.someStrict = createSelectedScripts(RecommendationLevel.Strict, 'strict-some-1', 'strict-some-2');
this.allStrict = [...this.someStrict, ...createSelectedScripts(RecommendationLevel.Strict, 'strict-all-1', 'strict-all-2')];
this.someUnrecommended = createSelectedScripts(undefined, 'unrecommended-some-1', 'unrecommended-some-2');
this.allUnrecommended = [...this.someUnrecommended, ...createSelectedScripts(undefined, 'unrecommended-all-1', 'unrecommended-all-2')];
this.all = [...this.allStandard, ...this.allStrict, ...this.allUnrecommended];
}
public generateState(selectedScripts: readonly SelectedScript[]) {
const allScripts = this.all.map((s) => s.script);
return new CategoryCollectionStateStub(allScripts)
.withSelectedScripts(selectedScripts);
}
}
function createSelectedScripts(level?: RecommendationLevel, ...ids: string[]) {
return ids.map((id) => new SelectedScript(new ScriptStub(id).withLevel(level), false));
return ids.map((id) => new SelectedScript(new ScriptStub(id).withLevel(level), false));
}

View File

@@ -6,127 +6,130 @@ import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { SelectionStateTestScenario } from './SelectionStateTestScenario';
describe('SelectionTypeHandler', () => {
describe('ctor', () => {
it('throws when state is undefined', () => {
// arrange
const expectedError = 'undefined state';
const state = undefined;
// act
const sut = () => new SelectionTypeHandler(state);
// assert
expect(sut).to.throw(expectedError);
});
describe('ctor', () => {
it('throws when state is undefined', () => {
// arrange
const expectedError = 'undefined state';
const state = undefined;
// act
const sut = () => new SelectionTypeHandler(state);
// assert
expect(sut).to.throw(expectedError);
});
describe('selectType', () => {
it('throws when type is custom', () => {
// arrange
const expectedError = 'cannot select custom type';
const scenario = new SelectionStateTestScenario();
const state = scenario.generateState([]);
const sut = new SelectionTypeHandler(state);
// act
const act = () => sut.selectType(SelectionType.Custom);
// assert
expect(act).to.throw(expectedError);
});
describe('select types as expected', () => {
// arrange
const scenario = new SelectionStateTestScenario();
const initialScriptsCases = [{
name: 'when nothing is selected',
initialScripts: [],
}, {
name: 'when some scripts are selected',
initialScripts: [...scenario.allStandard, ...scenario.someStrict],
}, {
name: 'when all scripts are selected',
initialScripts: scenario.all,
} ];
for (const initialScriptsCase of initialScriptsCases) {
describe(initialScriptsCase.name, () => {
const state = scenario.generateState(initialScriptsCase.initialScripts);
const sut = new SelectionTypeHandler(state);
const typeExpectations = [{
input: SelectionType.None,
output: [],
}, {
input: SelectionType.Standard,
output: scenario.allStandard,
}, {
input: SelectionType.Strict,
output: [...scenario.allStandard, ...scenario.allStrict],
}, {
input: SelectionType.All,
output: scenario.all,
}];
for (const expectation of typeExpectations) {
// act
it(`${SelectionType[expectation.input]} returns as expected`, () => {
sut.selectType(expectation.input);
// assert
const actual = state.selection.selectedScripts;
const expected = expectation.output;
expect(scrambledEqual(actual, expected));
});
}
});
}
});
});
describe('selectType', () => {
it('throws when type is custom', () => {
// arrange
const expectedError = 'cannot select custom type';
const scenario = new SelectionStateTestScenario();
const state = scenario.generateState([]);
const sut = new SelectionTypeHandler(state);
// act
const act = () => sut.selectType(SelectionType.Custom);
// assert
expect(act).to.throw(expectedError);
});
describe('getCurrentSelectionType', () => {
// arrange
const scenario = new SelectionStateTestScenario();
const testCases = [{
name: 'when nothing is selected',
selection: [],
expected: SelectionType.None,
}, {
name: 'when some standard scripts are selected',
selection: scenario.someStandard,
expected: SelectionType.Custom,
}, {
name: 'when all standard scripts are selected',
selection: scenario.allStandard,
expected: SelectionType.Standard,
}, {
name: 'when all standard and some strict scripts are selected',
selection: [...scenario.allStandard, ...scenario.someStrict],
expected: SelectionType.Custom,
}, {
name: 'when all standard and strict scripts are selected',
selection: [...scenario.allStandard, ...scenario.allStrict],
expected: SelectionType.Strict,
}, {
name: 'when strict scripts are selected but not standard',
selection: scenario.allStrict,
expected: SelectionType.Custom,
}, {
name: 'when all standard and strict, and some unrecommended are selected',
selection: [...scenario.allStandard, ...scenario.allStrict, ...scenario.someUnrecommended],
expected: SelectionType.Custom,
}, {
name: 'when all scripts are selected',
selection: scenario.all,
expected: SelectionType.All,
} ];
for (const testCase of testCases) {
it(testCase.name, () => {
const state = scenario.generateState(testCase.selection);
const sut = new SelectionTypeHandler(state);
// act
const actual = sut.getCurrentSelectionType();
// assert
expect(actual).to.deep.equal(testCase.expected,
`Actual: "${SelectionType[actual]}", expected: "${SelectionType[testCase.expected]}"` +
`\nSelection: ${printSelection()}`);
function printSelection() {
return `total: ${testCase.selection.length}\n` +
'scripts:\n' +
testCase.selection
.map((s) => `{ id: ${s.script.id}, level: ${s.script.level === undefined ? 'unknown' : RecommendationLevel[s.script.level]} }`)
.join(' | ');
}
describe('select types as expected', () => {
// arrange
const scenario = new SelectionStateTestScenario();
const initialScriptsCases = [{
name: 'when nothing is selected',
initialScripts: [],
}, {
name: 'when some scripts are selected',
initialScripts: [...scenario.allStandard, ...scenario.someStrict],
}, {
name: 'when all scripts are selected',
initialScripts: scenario.all,
}];
for (const initialScriptsCase of initialScriptsCases) {
describe(initialScriptsCase.name, () => {
const state = scenario.generateState(initialScriptsCase.initialScripts);
const sut = new SelectionTypeHandler(state);
const typeExpectations = [{
input: SelectionType.None,
output: [],
}, {
input: SelectionType.Standard,
output: scenario.allStandard,
}, {
input: SelectionType.Strict,
output: [...scenario.allStandard, ...scenario.allStrict],
}, {
input: SelectionType.All,
output: scenario.all,
}];
for (const expectation of typeExpectations) {
// act
it(`${SelectionType[expectation.input]} returns as expected`, () => {
sut.selectType(expectation.input);
// assert
const actual = state.selection.selectedScripts;
const expected = expectation.output;
expect(scrambledEqual(actual, expected));
});
}
}
});
}
});
});
describe('getCurrentSelectionType', () => {
// arrange
const scenario = new SelectionStateTestScenario();
const testCases = [{
name: 'when nothing is selected',
selection: [],
expected: SelectionType.None,
}, {
name: 'when some standard scripts are selected',
selection: scenario.someStandard,
expected: SelectionType.Custom,
}, {
name: 'when all standard scripts are selected',
selection: scenario.allStandard,
expected: SelectionType.Standard,
}, {
name: 'when all standard and some strict scripts are selected',
selection: [...scenario.allStandard, ...scenario.someStrict],
expected: SelectionType.Custom,
}, {
name: 'when all standard and strict scripts are selected',
selection: [...scenario.allStandard, ...scenario.allStrict],
expected: SelectionType.Strict,
}, {
name: 'when strict scripts are selected but not standard',
selection: scenario.allStrict,
expected: SelectionType.Custom,
}, {
name: 'when all standard and strict, and some unrecommended are selected',
selection: [...scenario.allStandard, ...scenario.allStrict, ...scenario.someUnrecommended],
expected: SelectionType.Custom,
}, {
name: 'when all scripts are selected',
selection: scenario.all,
expected: SelectionType.All,
}];
for (const testCase of testCases) {
it(testCase.name, () => {
const state = scenario.generateState(testCase.selection);
const sut = new SelectionTypeHandler(state);
// act
const actual = sut.getCurrentSelectionType();
// assert
expect(actual).to.deep.equal(
testCase.expected,
`Actual: "${SelectionType[actual]}", expected: "${SelectionType[testCase.expected]}"`
+ `\nSelection: ${printSelection()}`,
);
function printSelection() {
// eslint-disable-next-line prefer-template
return `total: ${testCase.selection.length}\n`
+ 'scripts:\n'
+ testCase.selection
.map((s) => `{ id: ${s.script.id}, level: ${s.script.level === undefined ? 'unknown' : RecommendationLevel[s.script.level]} }`)
.join(' | ');
}
});
}
});
});

View File

@@ -1,54 +1,53 @@
import 'mocha';
import { expect } from 'chai';
import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
import { hasDirective } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
import { NonCollapsing, hasDirective } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
const expectedAttributeName = 'data-interaction-does-not-collapse';
describe('NonCollapsingDirective', () => {
describe('NonCollapsing', () => {
it('adds expected attribute to the element when inserted', () => {
// arrange
const element = getElementMock();
// act
NonCollapsing.inserted(element, undefined, undefined, undefined);
// assert
expect(element.hasAttribute(expectedAttributeName));
});
describe('NonCollapsing', () => {
it('adds expected attribute to the element when inserted', () => {
// arrange
const element = getElementMock();
// act
NonCollapsing.inserted(element, undefined, undefined, undefined);
// assert
expect(element.hasAttribute(expectedAttributeName));
});
describe('hasDirective', () => {
it('returns true if the element has expected attribute', () => {
// arrange
const element = getElementMock();
element.setAttribute(expectedAttributeName, undefined);
// act
const actual = hasDirective(element);
// assert
expect(actual).to.equal(true);
});
it('returns true if the element has a parent with expected attribute', () => {
// arrange
const parent = getElementMock();
const element = getElementMock();
parent.appendChild(element);
element.setAttribute(expectedAttributeName, undefined);
// act
const actual = hasDirective(element);
// assert
expect(actual).to.equal(true);
});
it('returns false if nor the element or its parent has expected attribute', () => {
// arrange
const element = getElementMock();
// act
const actual = hasDirective(element);
// assert
expect(actual).to.equal(false);
});
});
describe('hasDirective', () => {
it('returns true if the element has expected attribute', () => {
// arrange
const element = getElementMock();
element.setAttribute(expectedAttributeName, undefined);
// act
const actual = hasDirective(element);
// assert
expect(actual).to.equal(true);
});
it('returns true if the element has a parent with expected attribute', () => {
// arrange
const parent = getElementMock();
const element = getElementMock();
parent.appendChild(element);
element.setAttribute(expectedAttributeName, undefined);
// act
const actual = hasDirective(element);
// assert
expect(actual).to.equal(true);
});
it('returns false if nor the element or its parent has expected attribute', () => {
// arrange
const element = getElementMock();
// act
const actual = hasDirective(element);
// assert
expect(actual).to.equal(false);
});
});
});
function getElementMock(): HTMLElement {
const element = document.createElement('div');
return element;
const element = document.createElement('div');
return element;
}

View File

@@ -1,8 +1,9 @@
import 'mocha';
import { expect } from 'chai';
import { getScriptNodeId, getScriptId, getCategoryNodeId, getCategoryId } from '@/presentation/components/Scripts/View/ScriptsTree/ScriptNodeParser';
import { parseSingleCategory,
parseAllCategories } from '@/presentation/components/Scripts/View/ScriptsTree/ScriptNodeParser';
import {
getScriptNodeId, getScriptId, getCategoryNodeId, getCategoryId, parseSingleCategory,
parseAllCategories,
} from '@/presentation/components/Scripts/View/ScriptsTree/ScriptNodeParser';
import { INode, NodeType } from '@/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/INode';
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
@@ -11,116 +12,116 @@ import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub';
describe('ScriptNodeParser', () => {
it('can convert script id and back', () => {
// arrange
const script = new ScriptStub('test');
// act
const nodeId = getScriptNodeId(script);
const scriptId = getScriptId(nodeId);
// assert
expect(scriptId).to.equal(script.id);
it('can convert script id and back', () => {
// arrange
const script = new ScriptStub('test');
// act
const nodeId = getScriptNodeId(script);
const scriptId = getScriptId(nodeId);
// assert
expect(scriptId).to.equal(script.id);
});
it('can convert category id and back', () => {
// arrange
const category = new CategoryStub(55);
// act
const nodeId = getCategoryNodeId(category);
const scriptId = getCategoryId(nodeId);
// assert
expect(scriptId).to.equal(category.id);
});
describe('parseSingleCategory', () => {
it('can parse when category has sub categories', () => {
// arrange
const categoryId = 31;
const firstSubCategory = new CategoryStub(11).withScriptIds('111', '112');
const secondSubCategory = new CategoryStub(categoryId)
.withCategory(new CategoryStub(33).withScriptIds('331', '331'))
.withCategory(new CategoryStub(44).withScriptIds('44'));
const collection = new CategoryCollectionStub().withAction(new CategoryStub(categoryId)
.withCategory(firstSubCategory)
.withCategory(secondSubCategory));
// act
const nodes = parseSingleCategory(categoryId, collection);
// assert
expect(nodes).to.have.lengthOf(2);
expectSameCategory(nodes[0], firstSubCategory);
expectSameCategory(nodes[1], secondSubCategory);
});
it('can convert category id and back', () => {
// arrange
const category = new CategoryStub(55);
// act
const nodeId = getCategoryNodeId(category);
const scriptId = getCategoryId(nodeId);
// assert
expect(scriptId).to.equal(category.id);
});
describe('parseSingleCategory', () => {
it('can parse when category has sub categories', () => {
// arrange
const categoryId = 31;
const firstSubCategory = new CategoryStub(11).withScriptIds('111', '112');
const secondSubCategory = new CategoryStub(categoryId)
.withCategory(new CategoryStub(33).withScriptIds('331', '331'))
.withCategory(new CategoryStub(44).withScriptIds('44'));
const collection = new CategoryCollectionStub().withAction(new CategoryStub(categoryId)
.withCategory(firstSubCategory)
.withCategory(secondSubCategory));
// act
const nodes = parseSingleCategory(categoryId, collection);
// assert
expect(nodes).to.have.lengthOf(2);
expectSameCategory(nodes[0], firstSubCategory);
expectSameCategory(nodes[1], secondSubCategory);
});
it('can parse when category has sub scripts', () => {
// arrange
const categoryId = 31;
const scripts = [ new ScriptStub('script1'), new ScriptStub('script2'), new ScriptStub('script3') ];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId).withScripts(...scripts));
// act
const nodes = parseSingleCategory(categoryId, collection);
// assert
expect(nodes).to.have.lengthOf(3);
expectSameScript(nodes[0], scripts[0]);
expectSameScript(nodes[1], scripts[1]);
expectSameScript(nodes[2], scripts[2]);
});
it('can parse when category has sub scripts', () => {
// arrange
const categoryId = 31;
const scripts = [new ScriptStub('script1'), new ScriptStub('script2'), new ScriptStub('script3')];
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(categoryId).withScripts(...scripts));
// act
const nodes = parseSingleCategory(categoryId, collection);
// assert
expect(nodes).to.have.lengthOf(3);
expectSameScript(nodes[0], scripts[0]);
expectSameScript(nodes[1], scripts[1]);
expectSameScript(nodes[2], scripts[2]);
});
});
it('parseAllCategories parses as expected', () => {
// arrange
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScriptIds('1, 2'))
.withAction(new CategoryStub(1).withCategories(
new CategoryStub(3).withScriptIds('3', '4'),
new CategoryStub(4).withCategory(new CategoryStub(5).withScriptIds('6')),
));
// act
const nodes = parseAllCategories(collection);
// assert
expect(nodes).to.have.lengthOf(2);
expectSameCategory(nodes[0], collection.actions[0]);
expectSameCategory(nodes[1], collection.actions[1]);
});
it('parseAllCategories parses as expected', () => {
// arrange
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScriptIds('1, 2'))
.withAction(new CategoryStub(1).withCategories(
new CategoryStub(3).withScriptIds('3', '4'),
new CategoryStub(4).withCategory(new CategoryStub(5).withScriptIds('6')),
));
// act
const nodes = parseAllCategories(collection);
// assert
expect(nodes).to.have.lengthOf(2);
expectSameCategory(nodes[0], collection.actions[0]);
expectSameCategory(nodes[1], collection.actions[1]);
});
});
function isReversible(category: ICategory): boolean {
if (category.scripts) {
return category.scripts.every((s) => s.canRevert());
}
return category.subCategories.every((c) => isReversible(c));
if (category.scripts) {
return category.scripts.every((s) => s.canRevert());
}
return category.subCategories.every((c) => isReversible(c));
}
function expectSameCategory(node: INode, category: ICategory): void {
expect(node.type).to.equal(NodeType.Category, getErrorMessage('type'));
expect(node.id).to.equal(getCategoryNodeId(category), getErrorMessage('id'));
expect(node.documentationUrls).to.equal(category.documentationUrls, getErrorMessage('documentationUrls'));
expect(node.text).to.equal(category.name, getErrorMessage('name'));
expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible'));
expect(node.children).to.have.lengthOf(category.scripts.length || category.subCategories.length, getErrorMessage('name'));
for (let i = 0; i < category.subCategories.length; i++) {
expectSameCategory(node.children[i], category.subCategories[i]);
}
for (let i = 0; i < category.scripts.length; i++) {
expectSameScript(node.children[i], category.scripts[i]);
}
function getErrorMessage(field: string) {
return `Unexpected node field: ${field}.\n` +
`\nActual node:\n${print(node)}` +
`\nExpected category:\n${print(category)}`;
}
expect(node.type).to.equal(NodeType.Category, getErrorMessage('type'));
expect(node.id).to.equal(getCategoryNodeId(category), getErrorMessage('id'));
expect(node.documentationUrls).to.equal(category.documentationUrls, getErrorMessage('documentationUrls'));
expect(node.text).to.equal(category.name, getErrorMessage('name'));
expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible'));
expect(node.children).to.have.lengthOf(category.scripts.length || category.subCategories.length, getErrorMessage('name'));
for (let i = 0; i < category.subCategories.length; i++) {
expectSameCategory(node.children[i], category.subCategories[i]);
}
for (let i = 0; i < category.scripts.length; i++) {
expectSameScript(node.children[i], category.scripts[i]);
}
function getErrorMessage(field: string) {
return `Unexpected node field: ${field}.\n`
+ `\nActual node:\n${print(node)}`
+ `\nExpected category:\n${print(category)}`;
}
}
function expectSameScript(node: INode, script: IScript): void {
expect(node.type).to.equal(NodeType.Script, getErrorMessage('type'));
expect(node.id).to.equal(getScriptNodeId(script), getErrorMessage('id'));
expect(node.documentationUrls).to.equal(script.documentationUrls, getErrorMessage('documentationUrls'));
expect(node.text).to.equal(script.name, getErrorMessage('name'));
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));
expect(node.children).to.equal(undefined);
function getErrorMessage(field: string) {
return `Unexpected node field: ${field}.` +
`\nActual node:\n${print(node)}\n` +
`\nExpected script:\n${print(script)}`;
}
expect(node.type).to.equal(NodeType.Script, getErrorMessage('type'));
expect(node.id).to.equal(getScriptNodeId(script), getErrorMessage('id'));
expect(node.documentationUrls).to.equal(script.documentationUrls, getErrorMessage('documentationUrls'));
expect(node.text).to.equal(script.name, getErrorMessage('name'));
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));
expect(node.children).to.equal(undefined);
function getErrorMessage(field: string) {
return `Unexpected node field: ${field}.`
+ `\nActual node:\n${print(node)}\n`
+ `\nExpected script:\n${print(script)}`;
}
}
function print(object: any) {
return JSON.stringify(object, null, 2);
function print(object: unknown) {
return JSON.stringify(object, null, 2);
}

View File

@@ -1,63 +1,63 @@
import 'mocha';
import { expect } from 'chai';
import { ILiquorTreeExistingNode } from 'liquor-tree';
import { NodeType, INode } from '@/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/INode';
import { NodePredicateFilter } from '@/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodePredicateFilter';
import { ILiquorTreeExistingNode } from 'liquor-tree';
describe('NodePredicateFilter', () => {
it('calls predicate with expected node', () => {
it('calls predicate with expected node', () => {
// arrange
const object: ILiquorTreeExistingNode = {
id: 'script',
data: {
text: 'script-text',
type: NodeType.Script,
documentationUrls: [],
isReversible: false,
},
states: undefined,
children: [],
};
const expected: INode = {
id: 'script',
text: 'script-text',
isReversible: false,
documentationUrls: [],
children: [],
type: NodeType.Script,
};
let actual: INode;
const predicate = (node: INode) => { actual = node; return true; };
const sut = new NodePredicateFilter(predicate);
// act
sut.matcher('nop query', object);
// assert
expect(actual).to.deep.equal(expected);
});
describe('returns result from the predicate', () => {
for (const expected of [false, true]) {
it(expected.toString(), () => {
// arrange
const object: ILiquorTreeExistingNode = {
id: 'script',
data: {
text: 'script-text',
type: NodeType.Script,
documentationUrls: [],
isReversible: false,
},
states: undefined,
children: [],
};
const expected: INode = {
id: 'script',
text: 'script-text',
isReversible: false,
documentationUrls: [],
children: [],
type: NodeType.Script,
};
let actual: INode;
const predicate = (node: INode) => { actual = node; return true; };
const sut = new NodePredicateFilter(predicate);
const sut = new NodePredicateFilter(() => expected);
// act
sut.matcher('nop query', object);
const actual = sut.matcher('nop query', getExistingNode());
// assert
expect(actual).to.deep.equal(expected);
});
describe('returns result from the predicate', () => {
for (const expected of [false, true]) {
it(expected.toString(), () => {
// arrange
const sut = new NodePredicateFilter(() => expected);
// act
const actual = sut.matcher('nop query', getExistingNode());
// assert
expect(actual).to.equal(expected);
});
}
});
expect(actual).to.equal(expected);
});
}
});
});
function getExistingNode(): ILiquorTreeExistingNode {
return {
id: 'script',
data: {
text: 'script-text',
type: NodeType.Script,
documentationUrls: [],
isReversible: false,
},
states: undefined,
children: [],
};
return {
id: 'script',
data: {
text: 'script-text',
type: NodeType.Script,
documentationUrls: [],
isReversible: false,
},
states: undefined,
children: [],
};
}

View File

@@ -5,199 +5,209 @@ import { NodeType } from '@/presentation/components/Scripts/View/ScriptsTree/Sel
import { getNewState } from '@/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater';
describe('NodeStateUpdater', () => {
describe('getNewState', () => {
describe('checked', () => {
describe('script node', () => {
it('true when selected', () => {
// arrange
const node = getScriptNode();
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(true);
});
it('false when unselected', () => {
// arrange
const node = getScriptNode();
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(false);
});
});
describe('category node', () => {
it('true when every child selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{ id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ],
},
{ id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('c') ],
},
],
};
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(true);
});
it('false when none of the children is selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{ id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ],
},
{ id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('c') ],
},
],
};
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(false);
});
it('false when some of the children is selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{
id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ],
},
{
id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('c') ],
},
],
};
const selectedScriptNodeIds = [ 'a', 'c', 'unrelated' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(false);
});
});
describe('getNewState', () => {
describe('checked', () => {
describe('script node', () => {
it('true when selected', () => {
// arrange
const node = getScriptNode();
const selectedScriptNodeIds = ['a', 'b', node.id, 'c'];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(true);
});
describe('indeterminate', () => {
describe('script node', () => {
it('false when selected', () => {
// arrange
const node = getScriptNode();
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(false);
});
it('false when not selected', () => {
// arrange
const node = getScriptNode();
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(false);
});
});
describe('category node', () => {
it('false when all children are selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{ id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ],
},
{ id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('c') ],
},
],
};
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(false);
});
it('true when all some are selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{ id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ],
},
{ id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('c') ],
},
],
};
const selectedScriptNodeIds = [ 'a' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(true);
});
it('false when no children are selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{ id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ],
},
{ id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('c') ],
},
],
};
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(false);
});
});
it('false when unselected', () => {
// arrange
const node = getScriptNode();
const selectedScriptNodeIds = ['a', 'b', 'c'];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(false);
});
});
describe('category node', () => {
it('true when every child selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{
id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('a'), getScriptNode('b')],
},
{
id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('c')],
},
],
};
const selectedScriptNodeIds = ['a', 'b', 'c'];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(true);
});
it('false when none of the children is selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{
id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('a'), getScriptNode('b')],
},
{
id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('c')],
},
],
};
const selectedScriptNodeIds = ['none', 'of', 'them', 'are', 'selected'];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(false);
});
it('false when some of the children is selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{
id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('a'), getScriptNode('b')],
},
{
id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('c')],
},
],
};
const selectedScriptNodeIds = ['a', 'c', 'unrelated'];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(false);
});
});
});
function getScriptNode(scriptNodeId: string = 'script'): ILiquorTreeNode {
return {
id: scriptNodeId,
data: {
type: NodeType.Script,
documentationUrls: [],
isReversible: false,
},
children: [],
};
}
describe('indeterminate', () => {
describe('script node', () => {
it('false when selected', () => {
// arrange
const node = getScriptNode();
const selectedScriptNodeIds = ['a', 'b', node.id, 'c'];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(false);
});
it('false when not selected', () => {
// arrange
const node = getScriptNode();
const selectedScriptNodeIds = ['a', 'b', 'c'];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(false);
});
});
describe('category node', () => {
it('false when all children are selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{
id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('a'), getScriptNode('b')],
},
{
id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('c')],
},
],
};
const selectedScriptNodeIds = ['a', 'b', 'c'];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(false);
});
it('true when all some are selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{
id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('a'), getScriptNode('b')],
},
{
id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('c')],
},
],
};
const selectedScriptNodeIds = ['a'];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(true);
});
it('false when no children are selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{
id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('a'), getScriptNode('b')],
},
{
id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [getScriptNode('c')],
},
],
};
const selectedScriptNodeIds = ['none', 'of', 'them', 'are', 'selected'];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(false);
});
});
});
});
function getScriptNode(scriptNodeId = 'script'): ILiquorTreeNode {
return {
id: scriptNodeId,
data: {
type: NodeType.Script,
documentationUrls: [],
isReversible: false,
},
children: [],
};
}
});

View File

@@ -1,142 +1,144 @@
import 'mocha';
import { expect } from 'chai';
import {
ILiquorTreeExistingNode, ILiquorTreeNewNode, ILiquorTreeNodeData, ICustomLiquorTreeData,
} from 'liquor-tree';
import { NodeType, INode } from '@/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/INode';
import { ILiquorTreeExistingNode, ILiquorTreeNewNode, ILiquorTreeNodeData, ICustomLiquorTreeData } from 'liquor-tree';
import { convertExistingToNode, toNewLiquorTreeNode } from '@/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeTranslator';
describe('NodeTranslator', () => {
it('convertExistingToNode', () => {
// arrange
const existingNode = getExistingNode();
const expected = getNode();
// act
const actual = convertExistingToNode(existingNode);
// assert
expect(actual).to.deep.equal(expected);
});
it('toNewLiquorTreeNode', () => {
// arrange
const node = getNode();
const expected = getNewNode();
// act
const actual = toNewLiquorTreeNode(node);
// assert
expect(actual).to.deep.equal(expected);
});
it('convertExistingToNode', () => {
// arrange
const existingNode = getExistingNode();
const expected = getNode();
// act
const actual = convertExistingToNode(existingNode);
// assert
expect(actual).to.deep.equal(expected);
});
it('toNewLiquorTreeNode', () => {
// arrange
const node = getNode();
const expected = getNewNode();
// act
const actual = toNewLiquorTreeNode(node);
// assert
expect(actual).to.deep.equal(expected);
});
});
function getNode(): INode {
return {
id: '1',
text: 'parentcategory',
return {
id: '1',
text: 'parentcategory',
isReversible: true,
type: NodeType.Category,
documentationUrls: ['parentcategory-url1', 'parentcategory-url2'],
children: [
{
id: '2',
text: 'subcategory',
isReversible: true,
documentationUrls: ['subcategory-url1', 'subcategory-url2'],
type: NodeType.Category,
documentationUrls: [ 'parentcategory-url1', 'parentcategory-url2 '],
children: [
{
id: '2',
text: 'subcategory',
isReversible: true,
documentationUrls: [ 'subcategory-url1', 'subcategory-url2 '],
type: NodeType.Category,
children: [
{
id: 'script1',
text: 'cool script 1',
isReversible: true,
documentationUrls: [ 'script1url1', 'script1url2'],
children: [],
type: NodeType.Script,
},
{
id: 'script2',
text: 'cool script 2',
isReversible: true,
documentationUrls: [ 'script2url1', 'script2url2'],
children: [],
type: NodeType.Script,
}],
}],
};
{
id: 'script1',
text: 'cool script 1',
isReversible: true,
documentationUrls: ['script1url1', 'script1url2'],
children: [],
type: NodeType.Script,
},
{
id: 'script2',
text: 'cool script 2',
isReversible: true,
documentationUrls: ['script2url1', 'script2url2'],
children: [],
type: NodeType.Script,
}],
}],
};
}
function getExpectedExistingNodeData(node: INode): ILiquorTreeNodeData {
return {
text: node.text,
type: node.type,
documentationUrls: node.documentationUrls,
isReversible: node.isReversible,
};
return {
text: node.text,
type: node.type,
documentationUrls: node.documentationUrls,
isReversible: node.isReversible,
};
}
function getExpectedNewNodeData(node: INode): ICustomLiquorTreeData {
return {
type: node.type,
documentationUrls: node.documentationUrls,
isReversible: node.isReversible,
};
return {
type: node.type,
documentationUrls: node.documentationUrls,
isReversible: node.isReversible,
};
}
function getExistingNode(): ILiquorTreeExistingNode {
const base = getNode();
return {
id: base.id,
data: getExpectedExistingNodeData(base),
const base = getNode();
return {
id: base.id,
data: getExpectedExistingNodeData(base),
states: undefined,
children: [
{
id: base.children[0].id,
data: getExpectedExistingNodeData(base.children[0]),
states: undefined,
children: [
{
id: base.children[0].id,
data: getExpectedExistingNodeData(base.children[0]),
{
id: base.children[0].children[0].id,
data: getExpectedExistingNodeData(base.children[0].children[0]),
states: undefined,
children: [
{
id: base.children[0].children[0].id,
data: getExpectedExistingNodeData(base.children[0].children[0]),
states: undefined,
children: [],
},
{
id: base.children[0].children[1].id,
data: getExpectedExistingNodeData(base.children[0].children[1]),
states: undefined,
children: [],
}],
}],
};
children: [],
},
{
id: base.children[0].children[1].id,
data: getExpectedExistingNodeData(base.children[0].children[1]),
states: undefined,
children: [],
}],
}],
};
}
function getNewNode(): ILiquorTreeNewNode {
const base = getNode();
const commonState = {
checked: false,
indeterminate: false,
};
return {
id: base.id,
text: base.text,
data: getExpectedNewNodeData(base),
const base = getNode();
const commonState = {
checked: false,
indeterminate: false,
};
return {
id: base.id,
text: base.text,
data: getExpectedNewNodeData(base),
state: commonState,
children: [
{
id: base.children[0].id,
text: base.children[0].text,
data: getExpectedNewNodeData(base.children[0]),
state: commonState,
children: [
{
id: base.children[0].id,
text: base.children[0].text,
data: getExpectedNewNodeData(base.children[0]),
{
id: base.children[0].children[0].id,
text: base.children[0].children[0].text,
data: getExpectedNewNodeData(base.children[0].children[0]),
state: commonState,
children: [
{
id: base.children[0].children[0].id,
text: base.children[0].children[0].text,
data: getExpectedNewNodeData(base.children[0].children[0]),
state: commonState,
children: [],
},
{
id: base.children[0].children[1].id,
text: base.children[0].children[1].text,
data: getExpectedNewNodeData(base.children[0].children[1]),
state: commonState,
children: [],
}],
}],
};
children: [],
},
{
id: base.children[0].children[1].id,
text: base.children[0].children[1].text,
data: getExpectedNewNodeData(base.children[0].children[1]),
state: commonState,
children: [],
}],
}],
};
}

View File

@@ -9,97 +9,99 @@ import { CategoryCollectionStub } from '@tests/unit/stubs/CategoryCollectionStub
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
describe('CategoryReverter', () => {
describe('getState', () => {
// arrange
const scripts = [
new ScriptStub('revertable').withRevertCode('REM revert me'),
new ScriptStub('revertable2').withRevertCode('REM revert me 2'),
];
const category = new CategoryStub(1).withScripts(...scripts);
const nodeId = getCategoryNodeId(category);
const collection = new CategoryCollectionStub().withAction(category);
describe('getState', () => {
// arrange
const scripts = [
new ScriptStub('revertable').withRevertCode('REM revert me'),
new ScriptStub('revertable2').withRevertCode('REM revert me 2'),
];
const category = new CategoryStub(1).withScripts(...scripts);
const nodeId = getCategoryNodeId(category);
const collection = new CategoryCollectionStub().withAction(category);
const sut = new CategoryReverter(nodeId, collection);
const testCases = [
{
name: 'false when subscripts are not reverted',
state: scripts.map((script) => new SelectedScript(script, false)),
expected: false,
},
{
name: 'false when some subscripts are reverted',
state: [new SelectedScript(scripts[0], false), new SelectedScript(scripts[0], true)],
expected: false,
},
{
name: 'false when subscripts are not reverted',
state: scripts.map((script) => new SelectedScript(script, true)),
expected: true,
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const actual = sut.getState(testCase.state);
// assert
expect(actual).to.equal(testCase.expected);
});
}
});
describe('selectWithRevertState', () => {
// arrange
const scripts = [
new ScriptStub('revertable').withRevertCode('REM revert me'),
new ScriptStub('revertable2').withRevertCode('REM revert me 2'),
];
const category = new CategoryStub(1).withScripts(...scripts);
const collection = new CategoryCollectionStub().withAction(category);
/* eslint-disable object-property-newline */
const testCases = [
{
name: 'selects with revert state when not selected',
selection: [],
revert: true, expectRevert: true,
},
{
name: 'selects with non-revert state when not selected',
selection: [],
revert: false, expectRevert: false,
},
{
name: 'switches when already selected with revert state',
selection: scripts.map((script) => new SelectedScript(script, true)),
revert: false, expectRevert: false,
},
{
name: 'switches when already selected with not revert state',
selection: scripts.map((script) => new SelectedScript(script, false)),
revert: true, expectRevert: true,
},
{
name: 'keeps revert state when already selected with revert state',
selection: scripts.map((script) => new SelectedScript(script, true)),
revert: true, expectRevert: true,
},
{
name: 'keeps revert state deselected when already selected wtih non revert state',
selection: scripts.map((script) => new SelectedScript(script, false)),
revert: false, expectRevert: false,
},
];
/* eslint-enable object-property-newline */
const nodeId = getCategoryNodeId(category);
for (const testCase of testCases) {
it(testCase.name, () => {
const selection = new UserSelection(collection, testCase.selection);
const sut = new CategoryReverter(nodeId, collection);
const testCases = [
{
name: 'false when subscripts are not reverted',
state: scripts.map((script) => new SelectedScript(script, false)),
expected: false,
},
{
name: 'false when some subscripts are reverted',
state: [new SelectedScript(scripts[0], false), new SelectedScript(scripts[0], true)],
expected: false,
},
{
name: 'false when subscripts are not reverted',
state: scripts.map((script) => new SelectedScript(script, true)),
expected: true,
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const actual = sut.getState(testCase.state);
// assert
expect(actual).to.equal(testCase.expected);
});
}
});
describe('selectWithRevertState', () => {
// arrange
const scripts = [
new ScriptStub('revertable').withRevertCode('REM revert me'),
new ScriptStub('revertable2').withRevertCode('REM revert me 2'),
];
const category = new CategoryStub(1).withScripts(...scripts);
const collection = new CategoryCollectionStub().withAction(category);
const testCases = [
{
name: 'selects with revert state when not selected',
selection: [],
revert: true, expectRevert: true,
},
{
name: 'selects with non-revert state when not selected',
selection: [],
revert: false, expectRevert: false,
},
{
name: 'switches when already selected with revert state',
selection: scripts.map((script) => new SelectedScript(script, true)),
revert: false, expectRevert: false,
},
{
name: 'switches when already selected with not revert state',
selection: scripts.map((script) => new SelectedScript(script, false)),
revert: true, expectRevert: true,
},
{
name: 'keeps revert state when already selected with revert state',
selection: scripts.map((script) => new SelectedScript(script, true)),
revert: true, expectRevert: true,
},
{
name: 'keeps revert state deselected when already selected wtih non revert state',
selection: scripts.map((script) => new SelectedScript(script, false)),
revert: false, expectRevert: false,
},
];
const nodeId = getCategoryNodeId(category);
for (const testCase of testCases) {
it(testCase.name, () => {
const selection = new UserSelection(collection, testCase.selection);
const sut = new CategoryReverter(nodeId, collection);
// act
sut.selectWithRevertState(testCase.revert, selection);
// assert
expect(sut.getState(selection.selectedScripts)).to.equal(testCase.expectRevert);
expect(selection.selectedScripts).has.lengthOf(2);
expect(selection.selectedScripts[0].id).equal(scripts[0].id);
expect(selection.selectedScripts[1].id).equal(scripts[1].id);
expect(selection.selectedScripts[0].revert).equal(testCase.expectRevert);
expect(selection.selectedScripts[1].revert).equal(testCase.expectRevert);
});
}
});
// act
sut.selectWithRevertState(testCase.revert, selection);
// assert
expect(sut.getState(selection.selectedScripts)).to.equal(testCase.expectRevert);
expect(selection.selectedScripts).has.lengthOf(2);
expect(selection.selectedScripts[0].id).equal(scripts[0].id);
expect(selection.selectedScripts[1].id).equal(scripts[1].id);
expect(selection.selectedScripts[0].revert).equal(testCase.expectRevert);
expect(selection.selectedScripts[1].revert).equal(testCase.expectRevert);
});
}
});
});

View File

@@ -10,38 +10,38 @@ import { CategoryStub } from '@tests/unit/stubs/CategoryStub';
import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
describe('ReverterFactory', () => {
describe('getReverter', () => {
it('gets CategoryReverter for category node', () => {
// arrange
const category = new CategoryStub(0).withScriptIds('55');
const node = getNodeStub(getCategoryNodeId(category), NodeType.Category);
const collection = new CategoryCollectionStub()
.withAction(category);
// act
const result = getReverter(node, collection);
// assert
expect(result instanceof CategoryReverter).to.equal(true);
});
it('gets ScriptReverter for script node', () => {
// arrange
const script = new ScriptStub('test');
const node = getNodeStub(getScriptNodeId(script), NodeType.Script);
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScript(script));
// act
const result = getReverter(node, collection);
// assert
expect(result instanceof ScriptReverter).to.equal(true);
});
describe('getReverter', () => {
it('gets CategoryReverter for category node', () => {
// arrange
const category = new CategoryStub(0).withScriptIds('55');
const node = getNodeStub(getCategoryNodeId(category), NodeType.Category);
const collection = new CategoryCollectionStub()
.withAction(category);
// act
const result = getReverter(node, collection);
// assert
expect(result instanceof CategoryReverter).to.equal(true);
});
function getNodeStub(nodeId: string, type: NodeType): INode {
return {
id: nodeId,
text: 'text',
isReversible: false,
documentationUrls: [],
children: [],
type,
};
}
it('gets ScriptReverter for script node', () => {
// arrange
const script = new ScriptStub('test');
const node = getNodeStub(getScriptNodeId(script), NodeType.Script);
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(0).withScript(script));
// act
const result = getReverter(node, collection);
// assert
expect(result instanceof ScriptReverter).to.equal(true);
});
});
function getNodeStub(nodeId: string, type: NodeType): INode {
return {
id: nodeId,
text: 'text',
isReversible: false,
documentationUrls: [],
children: [],
type,
};
}
});

View File

@@ -10,80 +10,82 @@ import { ScriptStub } from '@tests/unit/stubs/ScriptStub';
import { SelectedScriptStub } from '@tests/unit/stubs/SelectedScriptStub';
describe('ScriptReverter', () => {
describe('getState', () => {
it('false when script is not selected', () => {
// arrange
const script = new ScriptStub('id');
const nodeId = getScriptNodeId(script);
const sut = new ScriptReverter(nodeId);
// act
const actual = sut.getState([]);
// assert
expect(actual).to.equal(false);
});
it('false when script is selected but not reverted', () => {
// arrange
const scripts = [ new SelectedScriptStub('id'), new SelectedScriptStub('dummy') ];
const nodeId = getScriptNodeId(scripts[0].script);
const sut = new ScriptReverter(nodeId);
// act
const actual = sut.getState(scripts);
// assert
expect(actual).to.equal(false);
});
it('true when script is selected and reverted', () => {
// arrange
const scripts = [ new SelectedScriptStub('id', true), new SelectedScriptStub('dummy') ];
const nodeId = getScriptNodeId(scripts[0].script);
const sut = new ScriptReverter(nodeId);
// act
const actual = sut.getState(scripts);
// assert
expect(actual).to.equal(true);
});
describe('getState', () => {
it('false when script is not selected', () => {
// arrange
const script = new ScriptStub('id');
const nodeId = getScriptNodeId(script);
const sut = new ScriptReverter(nodeId);
// act
const actual = sut.getState([]);
// assert
expect(actual).to.equal(false);
});
describe('selectWithRevertState', () => {
// arrange
const script = new ScriptStub('id');
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(5).withScript(script));
const testCases = [
{
name: 'selects with revert state when not selected',
selection: [], revert: true, expectRevert: true,
},
{
name: 'selects with non-revert state when not selected',
selection: [], revert: false, expectRevert: false,
},
{
name: 'switches when already selected with revert state',
selection: [ new SelectedScript(script, true)], revert: false, expectRevert: false,
},
{
name: 'switches when already selected with not revert state',
selection: [ new SelectedScript(script, false)], revert: true, expectRevert: true,
},
{
name: 'keeps revert state when already selected with revert state',
selection: [ new SelectedScript(script, true)], revert: true, expectRevert: true,
},
{
name: 'keeps revert state deselected when already selected with non revert state',
selection: [ new SelectedScript(script, false)], revert: false, expectRevert: false,
},
];
const nodeId = getScriptNodeId(script);
for (const testCase of testCases) {
it(testCase.name, () => {
const selection = new UserSelection(collection, testCase.selection);
const sut = new ScriptReverter(nodeId);
// act
sut.selectWithRevertState(testCase.revert, selection);
// assert
expect(selection.isSelected(script.id)).to.equal(true);
expect(selection.selectedScripts[0].revert).equal(testCase.expectRevert);
});
}
it('false when script is selected but not reverted', () => {
// arrange
const scripts = [new SelectedScriptStub('id'), new SelectedScriptStub('dummy')];
const nodeId = getScriptNodeId(scripts[0].script);
const sut = new ScriptReverter(nodeId);
// act
const actual = sut.getState(scripts);
// assert
expect(actual).to.equal(false);
});
it('true when script is selected and reverted', () => {
// arrange
const scripts = [new SelectedScriptStub('id', true), new SelectedScriptStub('dummy')];
const nodeId = getScriptNodeId(scripts[0].script);
const sut = new ScriptReverter(nodeId);
// act
const actual = sut.getState(scripts);
// assert
expect(actual).to.equal(true);
});
});
describe('selectWithRevertState', () => {
// arrange
const script = new ScriptStub('id');
const collection = new CategoryCollectionStub()
.withAction(new CategoryStub(5).withScript(script));
/* eslint-disable object-property-newline */
const testCases = [
{
name: 'selects with revert state when not selected',
selection: [], revert: true, expectRevert: true,
},
{
name: 'selects with non-revert state when not selected',
selection: [], revert: false, expectRevert: false,
},
{
name: 'switches when already selected with revert state',
selection: [new SelectedScript(script, true)], revert: false, expectRevert: false,
},
{
name: 'switches when already selected with not revert state',
selection: [new SelectedScript(script, false)], revert: true, expectRevert: true,
},
{
name: 'keeps revert state when already selected with revert state',
selection: [new SelectedScript(script, true)], revert: true, expectRevert: true,
},
{
name: 'keeps revert state deselected when already selected with non revert state',
selection: [new SelectedScript(script, false)], revert: false, expectRevert: false,
},
];
/* eslint-enable object-property-newline */
const nodeId = getScriptNodeId(script);
for (const testCase of testCases) {
it(testCase.name, () => {
const selection = new UserSelection(collection, testCase.selection);
const sut = new ScriptReverter(nodeId);
// act
sut.selectWithRevertState(testCase.revert, selection);
// assert
expect(selection.isSelected(script.id)).to.equal(true);
expect(selection.selectedScripts[0].revert).equal(testCase.expectRevert);
});
}
});
});

View File

@@ -1,138 +1,158 @@
import 'mocha';
import { expect } from 'chai';
import { throttle, ITimer } from '@/presentation/components/Shared/Throttle';
import { throttle, ITimer, TimeoutType } from '@/presentation/components/Shared/Throttle';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
describe('throttle', () => {
it('throws if callback is undefined', () => {
// arrange
const expectedError = 'undefined callback';
const callback = undefined;
it('throws if callback is undefined', () => {
// arrange
const expectedError = 'undefined callback';
const callback = undefined;
// act
const act = () => throttle(callback, 500);
// assert
expect(act).to.throw(expectedError);
});
describe('throws if waitInMs is negative or zero', () => {
// arrange
const testCases = [
{ value: 0, expectedError: 'no delay to throttle' },
{ value: -2, expectedError: 'negative delay' },
];
const noopCallback = () => { /* do nothing */ };
for (const testCase of testCases) {
it(`"${testCase.value}" throws "${testCase.expectedError}"`, () => {
// act
const act = () => throttle(callback, 500);
const waitInMs = testCase.value;
const act = () => throttle(noopCallback, waitInMs);
// assert
expect(act).to.throw(expectedError);
});
describe('throws if waitInMs is negative or zero', () => {
// arrange
const testCases = [
{ value: 0, expectedError: 'no delay to throttle' },
{ value: -2, expectedError: 'negative delay' },
];
const callback = () => { return; };
for (const testCase of testCases) {
it(`"${testCase.value}" throws "${testCase.expectedError}"`, () => {
// act
const waitInMs = testCase.value;
const act = () => throttle(callback, waitInMs);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
it('should call the callback immediately', () => {
// arrange
const timer = new TimerMock();
let totalRuns = 0;
const callback = () => totalRuns++;
const throttleFunc = throttle(callback, 500, timer);
// act
throttleFunc();
// assert
expect(totalRuns).to.equal(1);
});
it('should call the callback again after the timeout', () => {
// arrange
const timer = new TimerMock();
let totalRuns = 0;
const callback = () => totalRuns++;
const waitInMs = 500;
const throttleFunc = throttle(callback, waitInMs, timer);
// act
throttleFunc();
totalRuns--; // So we don't count the initial run
throttleFunc();
timer.tickNext(waitInMs);
// assert
expect(totalRuns).to.equal(1);
});
it('should call the callback at most once at given time', () => {
// arrange
const timer = new TimerMock();
let totalRuns = 0;
const callback = () => totalRuns++;
const waitInMs = 500;
const totalCalls = 10;
const throttleFunc = throttle(callback, waitInMs, timer);
// act
for (let i = 0; i < totalCalls; i++) {
timer.setCurrentTime(waitInMs / totalCalls * i);
throttleFunc();
}
// assert
expect(totalRuns).to.equal(2); // one initial and one at the end
});
it('should call the callback as long as delay is waited', () => {
// arrange
const timer = new TimerMock();
let totalRuns = 0;
const callback = () => totalRuns++;
const waitInMs = 500;
const expectedTotalRuns = 10;
const throttleFunc = throttle(callback, waitInMs, timer);
// act
for (let i = 0; i < expectedTotalRuns; i++) {
throttleFunc();
timer.tickNext(waitInMs);
}
// assert
expect(totalRuns).to.equal(expectedTotalRuns);
});
it('should call arguments as expected', () => {
// arrange
const timer = new TimerMock();
const expected = [ 1, 2, 3 ];
const actual = new Array<number>();
const callback = (arg: number) => { actual.push(arg); };
const waitInMs = 500;
const throttleFunc = throttle(callback, waitInMs, timer);
// act
for (const arg of expected) {
throttleFunc(arg);
timer.tickNext(waitInMs);
}
// assert
expect(expected).to.deep.equal(actual);
});
expect(act).to.throw(testCase.expectedError);
});
}
});
it('should call the callback immediately', () => {
// arrange
const timer = new TimerMock();
let totalRuns = 0;
const callback = () => totalRuns++;
const throttleFunc = throttle(callback, 500, timer);
// act
throttleFunc();
// assert
expect(totalRuns).to.equal(1);
});
it('should call the callback again after the timeout', () => {
// arrange
const timer = new TimerMock();
let totalRuns = 0;
const callback = () => totalRuns++;
const waitInMs = 500;
const throttleFunc = throttle(callback, waitInMs, timer);
// act
throttleFunc();
totalRuns--; // So we don't count the initial run
throttleFunc();
timer.tickNext(waitInMs);
// assert
expect(totalRuns).to.equal(1);
});
it('should call the callback at most once at given time', () => {
// arrange
const timer = new TimerMock();
let totalRuns = 0;
const callback = () => totalRuns++;
const waitInMs = 500;
const totalCalls = 10;
const throttleFunc = throttle(callback, waitInMs, timer);
// act
for (let currentCall = 0; currentCall < totalCalls; currentCall++) {
const currentTime = (waitInMs / totalCalls) * currentCall;
timer.setCurrentTime(currentTime);
throttleFunc();
}
// assert
expect(totalRuns).to.equal(2); // one initial and one at the end
});
it('should call the callback as long as delay is waited', () => {
// arrange
const timer = new TimerMock();
let totalRuns = 0;
const callback = () => totalRuns++;
const waitInMs = 500;
const expectedTotalRuns = 10;
const throttleFunc = throttle(callback, waitInMs, timer);
// act
for (let i = 0; i < expectedTotalRuns; i++) {
throttleFunc();
timer.tickNext(waitInMs);
}
// assert
expect(totalRuns).to.equal(expectedTotalRuns);
});
it('should call arguments as expected', () => {
// arrange
const timer = new TimerMock();
const expected = [1, 2, 3];
const actual = new Array<number>();
const callback = (arg: number) => { actual.push(arg); };
const waitInMs = 500;
const throttleFunc = throttle(callback, waitInMs, timer);
// act
for (const arg of expected) {
throttleFunc(arg);
timer.tickNext(waitInMs);
}
// assert
expect(expected).to.deep.equal(actual);
});
});
class TimerMock implements ITimer {
private timeChanged = new EventSource<number>();
private subscriptions = new Array<IEventSubscription>();
private currentTime = 0;
public setTimeout(callback: () => void, ms: number): NodeJS.Timeout {
const runTime = this.currentTime + ms;
const subscription = this.timeChanged.on((time) => {
if (time >= runTime) {
callback();
subscription.unsubscribe();
}
});
this.subscriptions.push(subscription);
return (this.subscriptions.length - 1) as any;
}
public clearTimeout(timeoutId: NodeJS.Timeout): void {
this.subscriptions[timeoutId as any].unsubscribe();
}
public dateNow(): number {
return this.currentTime;
}
public tickNext(ms: number): void {
this.setCurrentTime(this.currentTime + ms);
}
public setCurrentTime(ms: number): void {
this.currentTime = ms;
this.timeChanged.notify(this.currentTime);
}
private timeChanged = new EventSource<number>();
private subscriptions = new Array<IEventSubscription>();
private currentTime = 0;
public setTimeout(callback: () => void, ms: number): TimeoutType {
const runTime = this.currentTime + ms;
const subscription = this.timeChanged.on((time) => {
if (time >= runTime) {
callback();
subscription.unsubscribe();
}
});
this.subscriptions.push(subscription);
const id = this.subscriptions.length - 1;
return TimerMock.mockTimeout(id);
}
public clearTimeout(timeoutId: TimeoutType): void {
this.subscriptions[+timeoutId].unsubscribe();
}
public dateNow(): number {
return this.currentTime;
}
public tickNext(ms: number): void {
this.setCurrentTime(this.currentTime + ms);
}
public setCurrentTime(ms: number): void {
this.currentTime = ms;
this.timeChanged.notify(this.currentTime);
}
private static mockTimeout(subscriptionId: number): TimeoutType {
const throwNodeSpecificCode = () => { throw new Error('node specific code'); };
return {
[Symbol.toPrimitive]: () => subscriptionId,
hasRef: throwNodeSpecificCode,
refresh: throwNodeSpecificCode,
ref: throwNodeSpecificCode,
unref: throwNodeSpecificCode,
};
}
}