This commit introduces a batched debounce mechanism for managing user
selection state changes. It effectively reduces unnecessary processing
during rapid script checking, preventing multiple triggers for code
compilation and UI rendering.
Key improvements include:
- Enhanced performance, especially noticeable when selecting large
categories. This update resolves minor UI freezes experienced when
selecting categories with numerous scripts.
- Correction of a bug where the code area only highlighted the last
selected script when multiple scripts were chosen.
Other changes include:
- Timing functions:
- Create a `Timing` folder for `throttle` and the new
`batchedDebounce` functions.
- Move these functions to the application layer from the presentation
layer, reflecting their application-wide use.
- Refactor existing code for improved clarity, naming consistency, and
adherence to new naming conventions.
- Add missing unit tests.
- `UserSelection`:
- State modifications in `UserSelection` now utilize a singular object
inspired by the CQRS pattern, enabling batch updates and flexible
change configurations, thereby simplifying change management.
- Remove the `I` prefix from related interfaces to align with new coding
standards.
- Refactor related code for better testability in isolation with
dependency injection.
- Repository:
- Move repository abstractions to the application layer.
- Improve repository abstraction to combine `ReadonlyRepository` and
`MutableRepository` interfaces.
- E2E testing:
- Introduce E2E tests to validate the correct batch selection
behavior.
- Add a specialized data attribute in `TheCodeArea.vue` for improved
testability.
- Reorganize shared Cypress functions for a more idiomatic Cypress
approach.
- Improve test documentation with related information.
- `SelectedScript`:
- Create an abstraction for simplified testability.
- Introduce `SelectedScriptStub` in tests as a substitute for the
actual object.
96 lines
3.8 KiB
TypeScript
96 lines
3.8 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
|
|
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
|
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
|
import { IScript } from '@/domain/IScript';
|
|
import { UseUserSelectionStateStub } from '@tests/unit/shared/Stubs/UseUserSelectionStateStub';
|
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
|
|
|
describe('useSelectedScriptNodeIds', () => {
|
|
it('returns an empty array when no scripts are selected', () => {
|
|
// arrange
|
|
const { useSelectionStateStub, returnObject } = runHook();
|
|
useSelectionStateStub.withSelectedScripts([]);
|
|
// act
|
|
const actualIds = returnObject.selectedScriptNodeIds.value;
|
|
// assert
|
|
expect(actualIds).to.have.lengthOf(0);
|
|
});
|
|
describe('returns correct node IDs for selected scripts', () => {
|
|
it('immediately', () => {
|
|
// arrange
|
|
const selectedScripts = [
|
|
new SelectedScriptStub(new ScriptStub('id-1')),
|
|
new SelectedScriptStub(new ScriptStub('id-2')),
|
|
];
|
|
const parsedNodeIds = new Map<IScript, string>([
|
|
[selectedScripts[0].script, 'expected-id-1'],
|
|
[selectedScripts[1].script, 'expected-id-2'],
|
|
]);
|
|
const useSelectionStateStub = new UseUserSelectionStateStub()
|
|
.withSelectedScripts(selectedScripts);
|
|
const { returnObject } = runHook({
|
|
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
|
useSelectionState: useSelectionStateStub,
|
|
});
|
|
// act
|
|
const actualIds = returnObject.selectedScriptNodeIds.value;
|
|
// assert
|
|
const expectedNodeIds = [...parsedNodeIds.values()];
|
|
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
|
expect(actualIds).to.include.members(expectedNodeIds);
|
|
});
|
|
it('when the selection state changes', () => {
|
|
// arrange
|
|
const initialScripts = [];
|
|
const changedScripts = [
|
|
new SelectedScriptStub(new ScriptStub('id-1')),
|
|
new SelectedScriptStub(new ScriptStub('id-2')),
|
|
];
|
|
const parsedNodeIds = new Map<IScript, string>([
|
|
[changedScripts[0].script, 'expected-id-1'],
|
|
[changedScripts[1].script, 'expected-id-2'],
|
|
]);
|
|
const useSelectionStateStub = new UseUserSelectionStateStub()
|
|
.withSelectedScripts(initialScripts);
|
|
const { returnObject } = runHook({
|
|
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
|
useSelectionState: useSelectionStateStub,
|
|
});
|
|
// act
|
|
useSelectionStateStub.withSelectedScripts(changedScripts);
|
|
const actualIds = returnObject.selectedScriptNodeIds.value;
|
|
// assert
|
|
const expectedNodeIds = [...parsedNodeIds.values()];
|
|
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
|
expect(actualIds).to.include.members(expectedNodeIds);
|
|
});
|
|
});
|
|
});
|
|
|
|
type ScriptNodeIdParser = typeof getScriptNodeId;
|
|
|
|
function createNodeIdParserFromMap(scriptToIdMap: Map<IScript, string>): ScriptNodeIdParser {
|
|
return (script) => {
|
|
const expectedId = scriptToIdMap.get(script);
|
|
if (!expectedId) {
|
|
throw new Error(`No mapped ID for script: ${JSON.stringify(script)}`);
|
|
}
|
|
return expectedId;
|
|
};
|
|
}
|
|
|
|
function runHook(scenario?: {
|
|
readonly scriptNodeIdParser?: ScriptNodeIdParser,
|
|
readonly useSelectionState?: UseUserSelectionStateStub,
|
|
}) {
|
|
const useSelectionStateStub = scenario?.useSelectionState ?? new UseUserSelectionStateStub();
|
|
const nodeIdParser: ScriptNodeIdParser = scenario?.scriptNodeIdParser
|
|
?? ((script) => script.id);
|
|
const returnObject = useSelectedScriptNodeIds(useSelectionStateStub.get(), nodeIdParser);
|
|
return {
|
|
returnObject,
|
|
useSelectionStateStub,
|
|
};
|
|
}
|