fix script revert activating recommendation level

Reverting any single of the scripts from standard recommendation pool
shows "Standard" selection as selected which is wrong. This commit fixes
it, refactors selection handling in a separate class and it also adds
missing tests. It removes UserSelection.totalSelected propertty in favor of using
UserSelection.selectedScripts.length to provide unified way of accessing
the information.
This commit is contained in:
undergroundwires
2021-04-17 14:34:29 +01:00
parent aea04e5f7c
commit a2f10857e2
13 changed files with 384 additions and 109 deletions

View File

@@ -55,7 +55,7 @@ describe('CategoryCollectionState', () => {
const collection = new CategoryCollectionStub();
const sut = new CategoryCollectionState(collection);
// act
const actual = sut.selection.totalSelected;
const actual = sut.selection.selectedScripts.length;
// assert
expect(actual).to.equal(0);
});
@@ -68,7 +68,7 @@ describe('CategoryCollectionState', () => {
// act
sut.selection.selectAll();
// assert
expect(sut.selection.totalSelected).to.equal(1);
expect(sut.selection.selectedScripts.length).to.equal(1);
expect(sut.selection.isSelected(expectedScript.id)).to.equal(true);
});
});

View File

@@ -152,7 +152,6 @@ describe('UserSelection', () => {
// act
sut.removeAllInCategory(categoryId);
// assert
expect(sut.totalSelected).to.equal(0);
expect(sut.selectedScripts.length).to.equal(0);
});
it('removes existing some exists', () => {
@@ -167,7 +166,6 @@ describe('UserSelection', () => {
// act
sut.removeAllInCategory(categoryId);
// assert
expect(sut.totalSelected).to.equal(0);
expect(sut.selectedScripts.length).to.equal(0);
});
});

View File

@@ -0,0 +1,32 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { CategoryCollectionStateStub } from '@tests/unit/stubs/CategoryCollectionStateStub';
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);
}
}
function createSelectedScripts(level?: RecommendationLevel, ...ids: string[]) {
return ids.map((id) => new SelectedScript(new ScriptStub(id).withLevel(level), false));
}

View File

@@ -0,0 +1,132 @@
import 'mocha';
import { expect } from 'chai';
import { SelectionType, SelectionTypeHandler } from '@/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler';
import { scrambledEqual } from '@/application/Common/Array';
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('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('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(' | ');
}
});
}
});
});

View File

@@ -0,0 +1,9 @@
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { EventSource } from '@/infrastructure/Events/EventSource';
export class ApplicationCodeStub implements IApplicationCode {
public changed: IEventSource<ICodeChangedEvent> = new EventSource<ICodeChangedEvent>();
public current: string = '';
}

View File

@@ -0,0 +1,30 @@
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { CategoryCollectionStub } from './CategoryCollectionStub';
import { UserSelectionStub } from './UserSelectionStub';
import { UserFilterStub } from './UserFilterStub';
import { ApplicationCodeStub } from './ApplicationCodeStub';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IScript } from '@/domain/IScript';
import { CategoryStub } from './CategoryStub';
export class CategoryCollectionStateStub implements ICategoryCollectionState {
public readonly code: IApplicationCode = new ApplicationCodeStub();
public readonly filter: IUserFilter = new UserFilterStub();
public readonly os = OperatingSystem.Windows;
public readonly collection: CategoryCollectionStub;
public readonly selection: UserSelectionStub;
constructor(readonly allScripts: IScript[]) {
this.selection = new UserSelectionStub(allScripts);
this.collection = new CategoryCollectionStub()
.withOs(this.os)
.withTotalScripts(this.allScripts.length)
.withAction(new CategoryStub(0).withScripts(...allScripts));
}
public withSelectedScripts(initialScripts: readonly SelectedScript[]) {
this.selection.withSelectedScripts(initialScripts);
return this;
}
}

View File

@@ -5,6 +5,7 @@ import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { ScriptStub } from './ScriptStub';
import { ScriptingDefinitionStub } from './ScriptingDefinitionStub';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
export class CategoryCollectionStub implements ICategoryCollection {
public scripting: IScriptingDefinition = new ScriptingDefinitionStub();
@@ -36,14 +37,16 @@ export class CategoryCollectionStub implements ICategoryCollection {
}
public findCategory(categoryId: number): ICategory {
return this.getAllCategories().find(
(category) => category.id === categoryId);
return this.getAllCategories()
.find((category) => category.id === categoryId);
}
public getScriptsByLevel(): readonly IScript[] {
throw new Error('Method not implemented: getScriptsByLevel');
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
return this.getAllScripts()
.filter((script) => script.level !== undefined && script.level <= level);
}
public findScript(scriptId: string): IScript {
return this.getAllScripts().find((script) => scriptId === script.id);
return this.getAllScripts()
.find((script) => scriptId === script.id);
}
public getAllScripts(): ReadonlyArray<IScript> {
const scripts = [];
@@ -79,9 +82,7 @@ function getSubCategoriesRecursively(category: ICategory): ReadonlyArray<ICatego
function getScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
const categoryScripts = [];
if (category.scripts) {
for (const script of category.scripts) {
categoryScripts.push(script);
}
categoryScripts.push(...category.scripts);
}
if (category.subCategories) {
for (const subCategory of category.subCategories) {

View File

@@ -0,0 +1,15 @@
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
export class UserFilterStub implements IUserFilter {
public currentFilter: IFilterResult;
public filtered: IEventSource<IFilterResult>;
public filterRemoved: IEventSource<void>;
public setFilter(): void {
throw new Error('Method not implemented.');
}
public removeFilter(): void {
throw new Error('Method not implemented.');
}
}

View File

@@ -0,0 +1,49 @@
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IScript } from '@/domain/IScript';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { EventSource } from '@/infrastructure/Events/EventSource';
export class UserSelectionStub implements IUserSelection {
public readonly changed: IEventSource<readonly SelectedScript[]> = new EventSource<readonly SelectedScript[]>();
public selectedScripts: readonly SelectedScript[] = [];
constructor(private readonly allScripts: readonly IScript[]) {
}
public withSelectedScripts(selectedScripts: readonly SelectedScript[]) {
this.selectedScripts = selectedScripts;
}
public areAllSelected(): boolean {
throw new Error('Method not implemented.');
}
public isAnySelected(): boolean {
throw new Error('Method not implemented.');
}
public removeAllInCategory(): void {
throw new Error('Method not implemented.');
}
public addOrUpdateAllInCategory(): void {
throw new Error('Method not implemented.');
}
public addSelectedScript(): void {
throw new Error('Method not implemented.');
}
public addOrUpdateSelectedScript(): void {
throw new Error('Method not implemented.');
}
public removeSelectedScript(): void {
throw new Error('Method not implemented.');
}
public selectOnly(scripts: ReadonlyArray<IScript>): void {
this.selectedScripts = scripts.map((s) => new SelectedScript(s, false));
}
public isSelected(): boolean {
throw new Error('Method not implemented.');
}
public selectAll(): void {
this.selectOnly(this.allScripts);
}
public deselectAll(): void {
this.selectedScripts = [];
}
}