Fix loss of tree node state when switching views
This commit fixes an issue where the check state of categories was lost when toggling between card and tree views. This is solved by immediately emitting node state changes for all nodes. This ensures consistent view transitions without any loss of node state information. Furthermore, this commit includes added unit tests for the modified code sections.
This commit is contained in:
19
tests/unit/shared/Stubs/DelaySchedulerStub.ts
Normal file
19
tests/unit/shared/Stubs/DelaySchedulerStub.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { DelayScheduler } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/DelayScheduler';
|
||||
|
||||
export class DelaySchedulerStub implements DelayScheduler {
|
||||
public nextCallback: () => void | undefined = undefined;
|
||||
|
||||
public scheduleNext(callback: () => void): void {
|
||||
this.nextCallback = callback;
|
||||
}
|
||||
|
||||
public runNextScheduled(): void {
|
||||
if (!this.nextCallback) {
|
||||
throw new Error('no callback is scheduled');
|
||||
}
|
||||
// Store the callback to prevent changes to this.nextCallback during execution
|
||||
const callback = this.nextCallback;
|
||||
this.nextCallback = undefined;
|
||||
callback();
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export class EventSubscriptionCollectionStub
|
||||
methodName: 'unsubscribeAllAndRegister',
|
||||
args: [subscriptions],
|
||||
});
|
||||
this.unsubscribeAll();
|
||||
this.register(subscriptions);
|
||||
// Not calling other methods to avoid registering method calls.
|
||||
this.subscriptions.splice(0, this.subscriptions.length, ...subscriptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,9 @@ export class HierarchyAccessStub implements HierarchyAccess {
|
||||
this.isBranchNode = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withIsLeafNode(value: boolean): this {
|
||||
this.isLeafNode = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
48
tests/unit/shared/Stubs/NodeStateChangeEventArgsStub.ts
Normal file
48
tests/unit/shared/Stubs/NodeStateChangeEventArgsStub.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { NodeStateChangeEventArgs } from '@/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator';
|
||||
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
|
||||
import { TreeNodeStub } from './TreeNodeStub';
|
||||
import { TreeNodeStateDescriptorStub } from './TreeNodeStateDescriptorStub';
|
||||
import { HierarchyAccessStub } from './HierarchyAccessStub';
|
||||
|
||||
export class NodeStateChangeEventArgsStub implements NodeStateChangeEventArgs {
|
||||
public node: TreeNode = new TreeNodeStub()
|
||||
.withId(`[${NodeStateChangeEventArgsStub.name}] node-stub`);
|
||||
|
||||
public newState: TreeNodeStateDescriptor = new TreeNodeStateDescriptorStub();
|
||||
|
||||
public oldState?: TreeNodeStateDescriptor = new TreeNodeStateDescriptorStub();
|
||||
|
||||
public withNode(node: TreeNode): this {
|
||||
this.node = node;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withNewState(newState: TreeNodeStateDescriptor): this {
|
||||
this.newState = newState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withOldState(oldState: TreeNodeStateDescriptor): this {
|
||||
this.oldState = oldState;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export function createChangeEvent(scenario: {
|
||||
readonly oldState?: TreeNodeStateDescriptor;
|
||||
readonly newState: TreeNodeStateDescriptor;
|
||||
readonly hierarchyBuilder?: (hierarchy: HierarchyAccessStub) => HierarchyAccessStub;
|
||||
}) {
|
||||
let nodeHierarchy = new HierarchyAccessStub();
|
||||
if (scenario.hierarchyBuilder) {
|
||||
nodeHierarchy = scenario.hierarchyBuilder(nodeHierarchy);
|
||||
}
|
||||
const changeEvent = new NodeStateChangeEventArgsStub()
|
||||
.withOldState(scenario.oldState)
|
||||
.withNewState(scenario.newState)
|
||||
.withNode(new TreeNodeStub()
|
||||
.withId(`[${createChangeEvent.name}] node-stub`)
|
||||
.withHierarchy(nodeHierarchy));
|
||||
return changeEvent;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
|
||||
import { NodeStateChangedEvent } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
|
||||
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
|
||||
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
|
||||
@@ -17,17 +16,4 @@ export class NodeStateChangedEventStub implements NodeStateChangedEvent {
|
||||
this.oldState = oldState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCheckStateChange(change: {
|
||||
readonly oldState: TreeNodeCheckState,
|
||||
readonly newState: TreeNodeCheckState,
|
||||
}) {
|
||||
return this
|
||||
.withOldState(
|
||||
new TreeNodeStateDescriptorStub().withCheckState(change.oldState),
|
||||
)
|
||||
.withNewState(
|
||||
new TreeNodeStateDescriptorStub().withCheckState(change.newState),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { QueryableNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes';
|
||||
import { TreeNodeStub } from './TreeNodeStub';
|
||||
|
||||
export class QueryableNodesStub implements QueryableNodes {
|
||||
public rootNodes: readonly TreeNode[];
|
||||
public rootNodes: readonly TreeNode[] = [
|
||||
new TreeNodeStub().withId(`[${QueryableNodesStub.name}] root-node-stub`),
|
||||
];
|
||||
|
||||
public flattenedNodes: readonly TreeNode[];
|
||||
public flattenedNodes: readonly TreeNode[] = [
|
||||
new TreeNodeStub().withId(`[${QueryableNodesStub.name}] flattened-node-stub-1`),
|
||||
new TreeNodeStub().withId(`[${QueryableNodesStub.name}] flattened-node-stub-2`),
|
||||
];
|
||||
|
||||
public getNodeById(): TreeNode {
|
||||
throw new Error('Method not implemented.');
|
||||
|
||||
@@ -3,7 +3,8 @@ import { SingleNodeFocusManager } from '@/presentation/components/Scripts/View/T
|
||||
import { TreeNodeStub } from './TreeNodeStub';
|
||||
|
||||
export class SingleNodeFocusManagerStub implements SingleNodeFocusManager {
|
||||
public currentSingleFocusedNode: TreeNode = new TreeNodeStub();
|
||||
public currentSingleFocusedNode: TreeNode = new TreeNodeStub()
|
||||
.withId(`[${SingleNodeFocusManagerStub.name}] focused-node-stub`);
|
||||
|
||||
setSingleFocus(): void { /* NOOP */ }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { QueryableNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes';
|
||||
import { TreeNodeCollection } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeCollection';
|
||||
import { EventSourceStub } from './EventSourceStub';
|
||||
import { QueryableNodesStub } from './QueryableNodesStub';
|
||||
|
||||
export class TreeNodeCollectionStub implements TreeNodeCollection {
|
||||
public nodes: QueryableNodes;
|
||||
public nodes: QueryableNodes = new QueryableNodesStub();
|
||||
|
||||
public nodesUpdated = new EventSourceStub<QueryableNodes>();
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ export function createTreeNodeParserStub() {
|
||||
if (result !== undefined) {
|
||||
return result.result;
|
||||
}
|
||||
return data.map(() => new TreeNodeStub());
|
||||
return data.map(() => new TreeNodeStub()
|
||||
.withId(`[${createTreeNodeParserStub.name}] parsed-node-stub`));
|
||||
};
|
||||
return {
|
||||
registerScenario,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { NodeStateChangedEvent, TreeNodeStateAccess, TreeNodeStateTransaction } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
|
||||
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
|
||||
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
|
||||
import { TreeNodeStateDescriptorStub } from './TreeNodeStateDescriptorStub';
|
||||
import { EventSourceStub } from './EventSourceStub';
|
||||
import { TreeNodeStateTransactionStub } from './TreeNodeStateTransactionStub';
|
||||
|
||||
export class TreeNodeStateAccessStub implements TreeNodeStateAccess {
|
||||
public isStateModificationRequested = false;
|
||||
|
||||
public current: TreeNodeStateDescriptor = new TreeNodeStateDescriptorStub();
|
||||
|
||||
public changed: EventSourceStub<NodeStateChangedEvent> = new EventSourceStub();
|
||||
@@ -32,6 +35,7 @@ export class TreeNodeStateAccessStub implements TreeNodeStateAccess {
|
||||
oldState,
|
||||
newState,
|
||||
});
|
||||
this.isStateModificationRequested = true;
|
||||
}
|
||||
|
||||
public triggerStateChangedEvent(event: NodeStateChangedEvent) {
|
||||
@@ -42,4 +46,26 @@ export class TreeNodeStateAccessStub implements TreeNodeStateAccess {
|
||||
this.current = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCurrentCheckState(checkState: TreeNodeCheckState): this {
|
||||
return this.withCurrent(
|
||||
new TreeNodeStateDescriptorStub()
|
||||
.withCheckState(checkState),
|
||||
);
|
||||
}
|
||||
|
||||
public withCurrentVisibility(isVisible: boolean): this {
|
||||
return this.withCurrent(
|
||||
new TreeNodeStateDescriptorStub()
|
||||
.withVisibility(isVisible),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function createAccessStubsFromCheckStates(
|
||||
states: readonly TreeNodeCheckState[],
|
||||
): TreeNodeStateAccessStub[] {
|
||||
return states.map(
|
||||
(checkState) => new TreeNodeStateAccessStub().withCurrentCheckState(checkState),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TreeNodeStateChangedEmittedEvent } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeNodeStateChangedEmittedEvent';
|
||||
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
|
||||
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
|
||||
import { ReadOnlyTreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { TreeNodeStateDescriptorStub } from './TreeNodeStateDescriptorStub';
|
||||
|
||||
export class TreeNodeStateChangedEmittedEventStub implements TreeNodeStateChangedEmittedEvent {
|
||||
public node: ReadOnlyTreeNode;
|
||||
|
||||
public oldState?: TreeNodeStateDescriptor;
|
||||
|
||||
public newState: TreeNodeStateDescriptor;
|
||||
|
||||
public withNode(node: ReadOnlyTreeNode): this {
|
||||
this.node = node;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withNewState(newState: TreeNodeStateDescriptor): this {
|
||||
this.newState = newState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withOldState(oldState: TreeNodeStateDescriptor): this {
|
||||
this.oldState = oldState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCheckStateChange(change: {
|
||||
readonly oldState: TreeNodeCheckState,
|
||||
readonly newState: TreeNodeCheckState,
|
||||
}) {
|
||||
return this
|
||||
.withOldState(
|
||||
new TreeNodeStateDescriptorStub().withCheckState(change.oldState),
|
||||
)
|
||||
.withNewState(
|
||||
new TreeNodeStateDescriptorStub().withCheckState(change.newState),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export class TreeNodeStateDescriptorStub implements TreeNodeStateDescriptor {
|
||||
|
||||
public isFocused = false;
|
||||
|
||||
public withFocusState(isFocused: boolean): this {
|
||||
public withFocus(isFocused: boolean): this {
|
||||
this.isFocused = isFocused;
|
||||
return this;
|
||||
}
|
||||
@@ -21,4 +21,9 @@ export class TreeNodeStateDescriptorStub implements TreeNodeStateDescriptor {
|
||||
this.checkState = checkState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withVisibility(isVisible: boolean): this {
|
||||
this.isVisible = isVisible;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,24 @@ import { TreeNodeStateAccess } from '@/presentation/components/Scripts/View/Tree
|
||||
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { NodeMetadataStub } from './NodeMetadataStub';
|
||||
import { HierarchyAccessStub } from './HierarchyAccessStub';
|
||||
import { TreeNodeStateAccessStub } from './TreeNodeStateAccessStub';
|
||||
|
||||
export class TreeNodeStub implements TreeNode {
|
||||
public state: TreeNodeStateAccess;
|
||||
public static fromStates(
|
||||
states: readonly TreeNodeStateAccess[],
|
||||
): TreeNodeStub[] {
|
||||
return states.map(
|
||||
(state) => new TreeNodeStub()
|
||||
.withId(`[${TreeNodeStub.fromStates.name}] node-stub`)
|
||||
.withState(state),
|
||||
);
|
||||
}
|
||||
|
||||
public state: TreeNodeStateAccess = new TreeNodeStateAccessStub();
|
||||
|
||||
public hierarchy: HierarchyAccess = new HierarchyAccessStub();
|
||||
|
||||
public id: string;
|
||||
public id = 'tree-node-stub-id';
|
||||
|
||||
public metadata?: object = new NodeMetadataStub();
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hook
|
||||
import { EventSubscriptionCollectionStub } from './EventSubscriptionCollectionStub';
|
||||
|
||||
export class UseAutoUnsubscribedEventsStub {
|
||||
public readonly events = new EventSubscriptionCollectionStub();
|
||||
|
||||
public get(): ReturnType<typeof useAutoUnsubscribedEvents> {
|
||||
return {
|
||||
events: new EventSubscriptionCollectionStub(),
|
||||
events: this.events,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
32
tests/unit/shared/Stubs/UseCurrentTreeNodesStub.ts
Normal file
32
tests/unit/shared/Stubs/UseCurrentTreeNodesStub.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
WatchSource, readonly, shallowRef, triggerRef,
|
||||
} from 'vue';
|
||||
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
||||
import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes';
|
||||
import { QueryableNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes';
|
||||
import { QueryableNodesStub } from './QueryableNodesStub';
|
||||
|
||||
export class UseCurrentTreeNodesStub {
|
||||
public treeWatcher: WatchSource<TreeRoot> | undefined;
|
||||
|
||||
private nodes = shallowRef<QueryableNodes>(new QueryableNodesStub());
|
||||
|
||||
public withQueryableNodes(nodes: QueryableNodes): this {
|
||||
this.nodes.value = nodes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public triggerNewNodes(nodes: QueryableNodes) {
|
||||
this.nodes.value = nodes;
|
||||
triggerRef(this.nodes);
|
||||
}
|
||||
|
||||
public get(): typeof useCurrentTreeNodes {
|
||||
return (treeWatcher: WatchSource<TreeRoot>) => {
|
||||
this.treeWatcher = treeWatcher;
|
||||
return {
|
||||
nodes: readonly(this.nodes),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
33
tests/unit/shared/Stubs/UseNodeStateChangeAggregatorStub.ts
Normal file
33
tests/unit/shared/Stubs/UseNodeStateChangeAggregatorStub.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { WatchSource } from 'vue';
|
||||
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
||||
import {
|
||||
NodeStateChangeEventArgs,
|
||||
NodeStateChangeEventCallback,
|
||||
useNodeStateChangeAggregator,
|
||||
} from '@/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator';
|
||||
|
||||
export class UseNodeStateChangeAggregatorStub {
|
||||
public callback: NodeStateChangeEventCallback | undefined;
|
||||
|
||||
public treeWatcher: WatchSource<TreeRoot> | undefined;
|
||||
|
||||
public onNodeStateChange(callback: NodeStateChangeEventCallback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public notifyChange(change: NodeStateChangeEventArgs) {
|
||||
if (!this.callback) {
|
||||
throw new Error('callback is not set');
|
||||
}
|
||||
this.callback(change);
|
||||
}
|
||||
|
||||
public get(): typeof useNodeStateChangeAggregator {
|
||||
return (treeWatcher: WatchSource<TreeRoot>) => {
|
||||
this.treeWatcher = treeWatcher;
|
||||
return {
|
||||
onNodeStateChange: this.onNodeStateChange.bind(this),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user