From a2f10857e2a8debb3ce01f79b0dfbe8649ea9a17 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Sat, 17 Apr 2021 14:34:29 +0100 Subject: [PATCH] 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. --- .../Context/State/Selection/IUserSelection.ts | 1 - .../Context/State/Selection/UserSelection.ts | 4 - .../Menu/Selector/SelectionTypeHandler.ts | 86 ++++++++++++ .../Scripts/Menu/Selector/TheSelector.vue | 112 +++------------ .../State/CategoryCollectionState.spec.ts | 4 +- .../State/Selection/UserSelection.spec.ts | 2 - .../Selector/SelectionStateTestScenario.ts | 32 +++++ .../Selector/SelectionTypeHandler.spec.ts | 132 ++++++++++++++++++ tests/unit/stubs/ApplicationCodeStub.ts | 9 ++ .../unit/stubs/CategoryCollectionStateStub.ts | 30 ++++ tests/unit/stubs/CategoryCollectionStub.ts | 17 +-- tests/unit/stubs/UserFilterStub.ts | 15 ++ tests/unit/stubs/UserSelectionStub.ts | 49 +++++++ 13 files changed, 384 insertions(+), 109 deletions(-) create mode 100644 src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts create mode 100644 tests/unit/presentation/components/Scripts/Menu/Selector/SelectionStateTestScenario.ts create mode 100644 tests/unit/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.spec.ts create mode 100644 tests/unit/stubs/ApplicationCodeStub.ts create mode 100644 tests/unit/stubs/CategoryCollectionStateStub.ts create mode 100644 tests/unit/stubs/UserFilterStub.ts create mode 100644 tests/unit/stubs/UserSelectionStub.ts diff --git a/src/application/Context/State/Selection/IUserSelection.ts b/src/application/Context/State/Selection/IUserSelection.ts index 12463114..0e23e7b3 100644 --- a/src/application/Context/State/Selection/IUserSelection.ts +++ b/src/application/Context/State/Selection/IUserSelection.ts @@ -6,7 +6,6 @@ import { IEventSource } from '@/infrastructure/Events/IEventSource'; export interface IUserSelection { readonly changed: IEventSource>; readonly selectedScripts: ReadonlyArray; - readonly totalSelected: number; areAllSelected(category: ICategory): boolean; isAnySelected(category: ICategory): boolean; removeAllInCategory(categoryId: number): void; diff --git a/src/application/Context/State/Selection/UserSelection.ts b/src/application/Context/State/Selection/UserSelection.ts index 9f3f132c..1bf64717 100644 --- a/src/application/Context/State/Selection/UserSelection.ts +++ b/src/application/Context/State/Selection/UserSelection.ts @@ -101,10 +101,6 @@ export class UserSelection implements IUserSelection { return this.scripts.getItems(); } - public get totalSelected(): number { - return this.scripts.getItems().length; - } - public selectAll(): void { for (const script of this.collection.getAllScripts()) { if (!this.scripts.exists(script.id)) { diff --git a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts new file mode 100644 index 00000000..2e630d08 --- /dev/null +++ b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts @@ -0,0 +1,86 @@ +import { IScript } from '@/domain/IScript'; +import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; +import { RecommendationLevel } from '@/domain/RecommendationLevel'; +import { scrambledEqual } from '@/application/Common/Array'; +import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; + +export enum SelectionType { + Standard, + Strict, + All, + None, + Custom, +} + +export class SelectionTypeHandler { + constructor(private readonly state: ICategoryCollectionState) { + if (!state) { throw new Error('undefined state'); } + } + public selectType(type: SelectionType) { + if (type === SelectionType.Custom) { + throw new Error('cannot select custom type'); + } + const selector = selectors.get(type); + selector.select(this.state); + } + public getCurrentSelectionType(): SelectionType { + for (const [type, selector] of Array.from(selectors.entries())) { + if (selector.isSelected(this.state)) { + return type; + } + } + return SelectionType.Custom; + } +} + +interface ISingleTypeHandler { + isSelected: (state: ICategoryCollectionState) => boolean; + select: (state: ICategoryCollectionState) => void; +} + +const selectors = new Map([ + [SelectionType.None, { + select: (state) => + state.selection.deselectAll(), + isSelected: (state) => + state.selection.selectedScripts.length === 0, + }], + [SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)], + [SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)], + [SelectionType.All, { + select: (state) => + state.selection.selectAll(), + isSelected: (state) => + state.selection.selectedScripts.length === state.collection.totalScripts, + }], +]); + +function getRecommendationLevelSelector(level: RecommendationLevel): ISingleTypeHandler { + return { + select: (state) => selectOnly(level, state), + isSelected: (state) => hasAllSelectedLevelOf(level, state), + }; +} + +function hasAllSelectedLevelOf(level: RecommendationLevel, state: ICategoryCollectionState) { + const scripts = state.collection.getScriptsByLevel(level); + const selectedScripts = state.selection.selectedScripts; + return areAllSelected(scripts, selectedScripts); +} + +function selectOnly(level: RecommendationLevel, state: ICategoryCollectionState) { + const scripts = state.collection.getScriptsByLevel(level); + state.selection.selectOnly(scripts); +} + +function areAllSelected( + expectedScripts: ReadonlyArray, + selection: ReadonlyArray): boolean { + selection = selection.filter((selected) => !selected.revert); + if (expectedScripts.length < selection.length) { + return false; + } + const selectedScriptIds = selection.map((script) => script.id); + const expectedScriptIds = expectedScripts.map((script) => script.id); + return scrambledEqual(selectedScriptIds, expectedScriptIds); +} diff --git a/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue b/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue index 02edb76b..7aaee3e8 100644 --- a/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue +++ b/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue @@ -5,8 +5,8 @@
@@ -15,8 +15,8 @@
{ + public async selectType(type: SelectionType) { if (this.currentSelection === type) { return; } - const context = await this.getCurrentContextAsync(); - selectType(context.state, type); + this.selectionTypeHandler.selectType(type); } protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void { - this.updateSelections(newState); - newState.selection.changed.on(() => this.updateSelections(newState)); + this.selectionTypeHandler = new SelectionTypeHandler(newState); + this.updateSelections(); + newState.selection.changed.on(() => this.updateSelections()); if (oldState) { - oldState.selection.changed.on(() => this.updateSelections(oldState)); + oldState.selection.changed.on(() => this.updateSelections()); } } - private updateSelections(state: ICategoryCollectionState) { - this.currentSelection = getCurrentSelectionState(state); + private updateSelections() { + this.currentSelection = this.selectionTypeHandler.getCurrentSelectionType(); } } -interface ITypeSelector { - isSelected: (state: ICategoryCollectionState) => boolean; - select: (state: ICategoryCollectionState) => void; -} - -const selectors = new Map([ - [SelectionState.None, { - select: (state) => - state.selection.deselectAll(), - isSelected: (state) => - state.selection.totalSelected === 0, - }], - [SelectionState.Standard, { - select: (state) => - state.selection.selectOnly( - state.collection.getScriptsByLevel(RecommendationLevel.Standard)), - isSelected: (state) => - hasAllSelectedLevelOf(RecommendationLevel.Standard, state), - }], - [SelectionState.Strict, { - select: (state) => - state.selection.selectOnly(state.collection.getScriptsByLevel(RecommendationLevel.Strict)), - isSelected: (state) => - hasAllSelectedLevelOf(RecommendationLevel.Strict, state), - }], - [SelectionState.All, { - select: (state) => - state.selection.selectAll(), - isSelected: (state) => - state.selection.totalSelected === state.collection.totalScripts, - }], -]); - -function selectType(state: ICategoryCollectionState, type: SelectionState) { - const selector = selectors.get(type); - selector.select(state); -} - -function getCurrentSelectionState(state: ICategoryCollectionState): SelectionState { - for (const [type, selector] of Array.from(selectors.entries())) { - if (selector.isSelected(state)) { - return type; - } - } - return SelectionState.Custom; -} - -function hasAllSelectedLevelOf(level: RecommendationLevel, state: ICategoryCollectionState) { - const scripts = state.collection.getScriptsByLevel(level); - const selectedScripts = state.selection.selectedScripts; - return areAllSelected(scripts, selectedScripts); -} - -function areAllSelected( - expectedScripts: ReadonlyArray, - selection: ReadonlyArray): boolean { - selection = selection.filter((selected) => !selected.revert); - if (expectedScripts.length < selection.length) { - return false; - } - const selectedScriptIds = selection.map((script) => script.id).sort(); - const expectedScriptIds = expectedScripts.map((script) => script.id).sort(); - return selectedScriptIds.every((id, index) => id === expectedScriptIds[index]); -}