From ae75059cc14db41f55dd2056f528442c7d319dd2 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Tue, 15 Aug 2023 18:11:30 +0200 Subject: [PATCH] Increase testability through dependency injection - Remove existing integration tests for hooks as they're redundant after this change. - Document the pattern in relevant documentation. - Introduce `useEnvironment` to increase testability. - Update components to inject dependencies rather than importing hooks directly. --- docs/presentation.md | 14 ++++ docs/tests.md | 1 + .../bootstrapping/DependencyProvider.ts | 28 +++++++ src/presentation/components/App.vue | 8 +- .../Instructions/InstructionList.vue | 5 +- .../Code/CodeButtons/TheCodeButtons.vue | 17 ++-- .../components/Code/TheCodeArea.vue | 8 +- .../Scripts/Menu/Selector/TheSelector.vue | 6 +- .../components/Scripts/Menu/TheOsChanger.vue | 9 +- .../Scripts/Menu/TheScriptsMenu.vue | 8 +- .../Scripts/View/Cards/CardList.vue | 5 +- .../Scripts/View/Cards/CardListItem.vue | 5 +- .../Scripts/View/ScriptsTree/ScriptsTree.vue | 6 +- .../SelectableTree/Node/RevertToggle.vue | 6 +- .../Scripts/View/TheScriptsView.vue | 8 +- .../components/Shared/Hooks/README.md | 5 ++ .../components/Shared/Hooks/UseApplication.ts | 15 +--- .../Shared/Hooks/UseCollectionState.ts | 13 +-- .../components/Shared/Hooks/UseEnvironment.ts | 8 ++ .../components/TheFooter/DownloadUrlList.vue | 6 +- .../TheFooter/DownloadUrlListItem.vue | 9 +- .../components/TheFooter/PrivacyPolicy.vue | 10 +-- .../components/TheFooter/TheFooter.vue | 12 +-- src/presentation/components/TheHeader.vue | 6 +- src/presentation/components/TheSearchBar.vue | 5 +- src/presentation/injectionSymbols.ts | 16 ++++ src/presentation/main.ts | 6 +- .../Shared/Hooks/UseApplication.spec.ts | 29 ------- .../Shared/Hooks/UseCollectionState.spec.ts | 83 ------------------- .../Shared/Hooks/UseApplication.spec.ts | 13 +++ .../Shared/Hooks/UseCollectionState.spec.ts | 12 +++ .../Shared/Hooks/UseEnvironment.spec.ts | 28 +++++++ 32 files changed, 209 insertions(+), 201 deletions(-) create mode 100644 src/presentation/bootstrapping/DependencyProvider.ts create mode 100644 src/presentation/components/Shared/Hooks/README.md create mode 100644 src/presentation/components/Shared/Hooks/UseEnvironment.ts create mode 100644 src/presentation/injectionSymbols.ts delete mode 100644 tests/integration/presentation/components/Shared/Hooks/UseApplication.spec.ts delete mode 100644 tests/integration/presentation/components/Shared/Hooks/UseCollectionState.spec.ts create mode 100644 tests/unit/presentation/components/Shared/Hooks/UseEnvironment.spec.ts diff --git a/docs/presentation.md b/docs/presentation.md index 49595164..194712fe 100644 --- a/docs/presentation.md +++ b/docs/presentation.md @@ -61,6 +61,20 @@ Stateful components can mutate and/or react to state changes (e.g., user selecti 📖 Refer to [architecture.md | Application State](./architecture.md#application-state) for an overview of event handling and [application.md | Application State](./presentation.md#application-state) for an in-depth understanding of state management in the application layer. +## Dependency injections + +The presentation layer uses Vue's native dependency injection system to increase testability and decouple components. + +To add a new dependency: + +1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into: + - **Singletons**: Shared across components, instantiated once. + - **Transients**: Factories yielding a new instance on every access. +2. **Provide the dependency**: Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. [`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies. +3. **Inject the dependency**: Use Vue's `inject` method alongside the defined symbol to incorporate the dependency into components. + - For singletons, invoke the factory method: `inject(symbolKey)()`. + - For transients, directly inject: `inject(symbolKey)`. + ## Shared UI components Shared UI components promote consistency and simplifies the creation of the front-end. diff --git a/docs/tests.md b/docs/tests.md index f93cb77f..b8f25245 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -18,6 +18,7 @@ Common aspects for all tests: - Unit tests test each component in isolation. - All unit tests goes under [`./tests/unit`](./../tests/unit). - They rely on [stubs](./../tests/unit/shared/Stubs) for isolation. +- Unit tests include also Vue component tests using `@vue/test-utils`. ### Unit tests structure diff --git a/src/presentation/bootstrapping/DependencyProvider.ts b/src/presentation/bootstrapping/DependencyProvider.ts new file mode 100644 index 00000000..199de1aa --- /dev/null +++ b/src/presentation/bootstrapping/DependencyProvider.ts @@ -0,0 +1,28 @@ +import { InjectionKey, provide } from 'vue'; +import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState'; +import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication'; +import { + useCollectionStateKey, useApplicationKey, useEnvironmentKey, +} from '@/presentation/injectionSymbols'; +import { IApplicationContext } from '@/application/Context/IApplicationContext'; +import { Environment } from '@/application/Environment/Environment'; + +export function provideDependencies(context: IApplicationContext) { + registerSingleton(useApplicationKey, useApplication(context.app)); + registerTransient(useCollectionStateKey, () => useCollectionState(context)); + registerSingleton(useEnvironmentKey, Environment.CurrentEnvironment); +} + +function registerSingleton( + key: InjectionKey, + value: T, +) { + provide(key, value); +} + +function registerTransient( + key: InjectionKey<() => T>, + factory: () => T, +) { + provide(key, factory); +} diff --git a/src/presentation/components/App.vue b/src/presentation/components/App.vue index b2c7b043..b8a87bcd 100644 --- a/src/presentation/components/App.vue +++ b/src/presentation/components/App.vue @@ -17,6 +17,10 @@ import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue'; import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue'; import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue'; import TheSearchBar from '@/presentation/components/TheSearchBar.vue'; +import { buildContext } from '@/application/Context/ApplicationContextFactory'; +import { provideDependencies } from '../bootstrapping/DependencyProvider'; + +const singletonAppContext = await buildContext(); export default defineComponent({ components: { @@ -26,8 +30,10 @@ export default defineComponent({ TheSearchBar, TheFooter, }, + setup() { + provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts + }, }); -