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.
This commit is contained in:
@@ -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.
|
📖 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
|
||||||
|
|
||||||
Shared UI components promote consistency and simplifies the creation of the front-end.
|
Shared UI components promote consistency and simplifies the creation of the front-end.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ Common aspects for all tests:
|
|||||||
- Unit tests test each component in isolation.
|
- Unit tests test each component in isolation.
|
||||||
- All unit tests goes under [`./tests/unit`](./../tests/unit).
|
- All unit tests goes under [`./tests/unit`](./../tests/unit).
|
||||||
- They rely on [stubs](./../tests/unit/shared/Stubs) for isolation.
|
- They rely on [stubs](./../tests/unit/shared/Stubs) for isolation.
|
||||||
|
- Unit tests include also Vue component tests using `@vue/test-utils`.
|
||||||
|
|
||||||
### Unit tests structure
|
### Unit tests structure
|
||||||
|
|
||||||
|
|||||||
28
src/presentation/bootstrapping/DependencyProvider.ts
Normal file
28
src/presentation/bootstrapping/DependencyProvider.ts
Normal file
@@ -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<T>(
|
||||||
|
key: InjectionKey<T>,
|
||||||
|
value: T,
|
||||||
|
) {
|
||||||
|
provide(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerTransient<T>(
|
||||||
|
key: InjectionKey<() => T>,
|
||||||
|
factory: () => T,
|
||||||
|
) {
|
||||||
|
provide(key, factory);
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@ import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
|
|||||||
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
|
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
|
||||||
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
||||||
import TheSearchBar from '@/presentation/components/TheSearchBar.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({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -26,8 +30,10 @@ export default defineComponent({
|
|||||||
TheSearchBar,
|
TheSearchBar,
|
||||||
TheFooter,
|
TheFooter,
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
provideDependencies(singletonAppContext); // In Vue 3.0 we can move it to main.ts
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -57,10 +57,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, PropType, computed,
|
defineComponent, PropType, computed,
|
||||||
|
inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
import { useApplicationKey } from '@/presentation/injectionSymbols';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
|
||||||
import CodeInstruction from './CodeInstruction.vue';
|
import CodeInstruction from './CodeInstruction.vue';
|
||||||
import { IInstructionListData } from './InstructionListData';
|
import { IInstructionListData } from './InstructionListData';
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { info } = useApplication();
|
const { info } = inject(useApplicationKey);
|
||||||
|
|
||||||
const appName = computed<string>(() => info.name);
|
const appName = computed<string>(() => info.name);
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import {
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
defineComponent, ref, computed, inject,
|
||||||
|
} from 'vue';
|
||||||
|
import { useCollectionStateKey, useEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
@@ -44,8 +46,6 @@ import IconButton from './IconButton.vue';
|
|||||||
import { IInstructionListData } from './Instructions/InstructionListData';
|
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||||
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
||||||
|
|
||||||
const isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -55,10 +55,11 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const {
|
const {
|
||||||
currentState, currentContext, onStateChange, events,
|
currentState, currentContext, onStateChange, events,
|
||||||
} = useCollectionState();
|
} = inject(useCollectionStateKey)();
|
||||||
|
const { isDesktop } = inject(useEnvironmentKey);
|
||||||
|
|
||||||
const areInstructionsVisible = ref(false);
|
const areInstructionsVisible = ref(false);
|
||||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os));
|
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop));
|
||||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||||
const hasCode = ref(false);
|
const hasCode = ref(false);
|
||||||
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
||||||
@@ -98,7 +99,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDesktopVersion,
|
isDesktopVersion: isDesktop,
|
||||||
canRun,
|
canRun,
|
||||||
hasCode,
|
hasCode,
|
||||||
instructions,
|
instructions,
|
||||||
@@ -121,7 +122,7 @@ function getDownloadInstructions(
|
|||||||
return getInstructions(os, fileName);
|
return getInstructions(os, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCanRunState(selectedOs: OperatingSystem): boolean {
|
function getCanRunState(selectedOs: OperatingSystem, isDesktopVersion: boolean): boolean {
|
||||||
const isRunningOnSelectedOs = selectedOs === Environment.CurrentEnvironment.os;
|
const isRunningOnSelectedOs = selectedOs === Environment.CurrentEnvironment.os;
|
||||||
return isDesktopVersion && isRunningOnSelectedOs;
|
return isDesktopVersion && isRunningOnSelectedOs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onUnmounted, onMounted } from 'vue';
|
import {
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
defineComponent, onUnmounted, onMounted, inject,
|
||||||
|
} from 'vue';
|
||||||
|
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
@@ -35,7 +37,7 @@ export default defineComponent({
|
|||||||
NonCollapsing,
|
NonCollapsing,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { onStateChange, currentState, events } = useCollectionState();
|
const { onStateChange, currentState, events } = inject(useCollectionStateKey)();
|
||||||
|
|
||||||
const editorId = 'codeEditor';
|
const editorId = 'codeEditor';
|
||||||
let editor: ace.Ace.Editor | undefined;
|
let editor: ace.Ace.Editor | undefined;
|
||||||
|
|||||||
@@ -65,8 +65,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref, inject } from 'vue';
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import MenuOptionList from '../MenuOptionList.vue';
|
import MenuOptionList from '../MenuOptionList.vue';
|
||||||
@@ -80,7 +80,7 @@ export default defineComponent({
|
|||||||
TooltipWrapper,
|
TooltipWrapper,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { modifyCurrentState, onStateChange, events } = useCollectionState();
|
const { modifyCurrentState, onStateChange, events } = inject(useCollectionStateKey)();
|
||||||
|
|
||||||
const currentSelection = ref(SelectionType.None);
|
const currentSelection = ref(SelectionType.None);
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,10 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, computed,
|
defineComponent, computed, inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
import { useApplicationKey, useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
|
||||||
import MenuOptionList from './MenuOptionList.vue';
|
import MenuOptionList from './MenuOptionList.vue';
|
||||||
import MenuOptionListItem from './MenuOptionListItem.vue';
|
import MenuOptionListItem from './MenuOptionListItem.vue';
|
||||||
|
|
||||||
@@ -31,8 +30,8 @@ export default defineComponent({
|
|||||||
MenuOptionListItem,
|
MenuOptionListItem,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { modifyCurrentContext, currentState } = useCollectionState();
|
const { modifyCurrentContext, currentState } = inject(useCollectionStateKey)();
|
||||||
const { application } = useApplication();
|
const { application } = inject(useApplicationKey);
|
||||||
|
|
||||||
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
|
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
|
||||||
application.getSupportedOsList() ?? [])
|
application.getSupportedOsList() ?? [])
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, onUnmounted } from 'vue';
|
import {
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
defineComponent, ref, onUnmounted, inject,
|
||||||
|
} from 'vue';
|
||||||
|
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import TheOsChanger from './TheOsChanger.vue';
|
import TheOsChanger from './TheOsChanger.vue';
|
||||||
import TheSelector from './Selector/TheSelector.vue';
|
import TheSelector from './Selector/TheSelector.vue';
|
||||||
@@ -24,7 +26,7 @@ export default defineComponent({
|
|||||||
TheViewChanger,
|
TheViewChanger,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { onStateChange, events } = useCollectionState();
|
const { onStateChange, events } = inject(useCollectionStateKey)();
|
||||||
|
|
||||||
const isSearching = ref(false);
|
const isSearching = ref(false);
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, ref, onMounted, onUnmounted, computed,
|
defineComponent, ref, onMounted, onUnmounted, computed,
|
||||||
|
inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||||
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
||||||
import { hasDirective } from './NonCollapsingDirective';
|
import { hasDirective } from './NonCollapsingDirective';
|
||||||
import CardListItem from './CardListItem.vue';
|
import CardListItem from './CardListItem.vue';
|
||||||
@@ -47,7 +48,7 @@ export default defineComponent({
|
|||||||
SizeObserver,
|
SizeObserver,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { currentState, onStateChange } = useCollectionState();
|
const { currentState, onStateChange } = inject(useCollectionStateKey)();
|
||||||
|
|
||||||
const width = ref<number>(0);
|
const width = ref<number>(0);
|
||||||
const categoryIds = computed<ReadonlyArray<number>>(() => currentState
|
const categoryIds = computed<ReadonlyArray<number>>(() => currentState
|
||||||
|
|||||||
@@ -50,8 +50,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, ref, watch, computed,
|
defineComponent, ref, watch, computed,
|
||||||
|
inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||||
import ScriptsTree from '@/presentation/components/Scripts/View/ScriptsTree/ScriptsTree.vue';
|
import ScriptsTree from '@/presentation/components/Scripts/View/ScriptsTree/ScriptsTree.vue';
|
||||||
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ export default defineComponent({
|
|||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const { events, onStateChange, currentState } = useCollectionState();
|
const { events, onStateChange, currentState } = inject(useCollectionStateKey)();
|
||||||
|
|
||||||
const isExpanded = computed({
|
const isExpanded = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, watch, ref,
|
defineComponent, watch, ref, inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
@@ -44,7 +44,7 @@ export default defineComponent({
|
|||||||
setup(props) {
|
setup(props) {
|
||||||
const {
|
const {
|
||||||
modifyCurrentState, currentState, onStateChange, events,
|
modifyCurrentState, currentState, onStateChange, events,
|
||||||
} = useCollectionState();
|
} = inject(useCollectionStateKey)();
|
||||||
|
|
||||||
const nodes = ref<ReadonlyArray<INodeContent>>([]);
|
const nodes = ref<ReadonlyArray<INodeContent>>([]);
|
||||||
const selectedNodeIds = ref<ReadonlyArray<string>>([]);
|
const selectedNodeIds = ref<ReadonlyArray<string>>([]);
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
PropType, defineComponent, ref, watch,
|
PropType, defineComponent, ref, watch,
|
||||||
computed,
|
computed, inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
|
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||||
import { IReverter } from './Reverter/IReverter';
|
import { IReverter } from './Reverter/IReverter';
|
||||||
import { INodeContent } from './INodeContent';
|
import { INodeContent } from './INodeContent';
|
||||||
import { getReverter } from './Reverter/ReverterFactory';
|
import { getReverter } from './Reverter/ReverterFactory';
|
||||||
@@ -31,7 +31,7 @@ export default defineComponent({
|
|||||||
setup(props) {
|
setup(props) {
|
||||||
const {
|
const {
|
||||||
currentState, modifyCurrentState, onStateChange, events,
|
currentState, modifyCurrentState, onStateChange, events,
|
||||||
} = useCollectionState();
|
} = inject(useCollectionStateKey)();
|
||||||
|
|
||||||
const isReverted = ref(false);
|
const isReverted = ref(false);
|
||||||
|
|
||||||
|
|||||||
@@ -34,14 +34,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, PropType, ref, computed,
|
defineComponent, PropType, ref, computed,
|
||||||
|
inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useApplicationKey, useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||||
import ScriptsTree from '@/presentation/components/Scripts/View/ScriptsTree/ScriptsTree.vue';
|
import ScriptsTree from '@/presentation/components/Scripts/View/ScriptsTree/ScriptsTree.vue';
|
||||||
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
|
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
|
||||||
import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType';
|
import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
|
||||||
|
|
||||||
/** Shows content of single category or many categories */
|
/** Shows content of single category or many categories */
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -56,8 +56,8 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { modifyCurrentState, onStateChange, events } = useCollectionState();
|
const { modifyCurrentState, onStateChange, events } = inject(useCollectionStateKey)();
|
||||||
const { info } = useApplication();
|
const { info } = inject(useApplicationKey);
|
||||||
|
|
||||||
const repositoryUrl = computed<string>(() => info.repositoryWebUrl);
|
const repositoryUrl = computed<string>(() => info.repositoryWebUrl);
|
||||||
const searchQuery = ref<string>();
|
const searchQuery = ref<string>();
|
||||||
|
|||||||
5
src/presentation/components/Shared/Hooks/README.md
Normal file
5
src/presentation/components/Shared/Hooks/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Hooks
|
||||||
|
|
||||||
|
This folder contains shared hooks used throughout the application.
|
||||||
|
|
||||||
|
To use the hooks, prefer using Vue-native `provide` / `inject` pattern to keep the components independently testable without side-effect.
|
||||||
@@ -1,16 +1,9 @@
|
|||||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
/* Application is always static */
|
export function useApplication(application: IApplication) {
|
||||||
let cachedApplication: IApplication;
|
if (!application) {
|
||||||
|
throw new Error('missing application');
|
||||||
// Running tests through Vue CLI throws 'Top-level-await is only supported in EcmaScript Modules'
|
}
|
||||||
// This is a temporary workaround until migrating to Vite
|
|
||||||
ApplicationFactory.Current.getApp().then((app) => {
|
|
||||||
cachedApplication = app;
|
|
||||||
});
|
|
||||||
|
|
||||||
export function useApplication(application: IApplication = cachedApplication) {
|
|
||||||
return {
|
return {
|
||||||
application,
|
application,
|
||||||
info: application.info,
|
info: application.info,
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import { ref, computed, readonly } from 'vue';
|
import { ref, computed, readonly } from 'vue';
|
||||||
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
|
||||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
|
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
|
||||||
|
|
||||||
let singletonContext: IApplicationContext;
|
export function useCollectionState(context: IApplicationContext) {
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('missing context');
|
||||||
|
}
|
||||||
|
|
||||||
// Running tests through Vue CLI throws 'Top-level-await is only supported in EcmaScript Modules'
|
|
||||||
// This is a temporary workaround until migrating to Vite
|
|
||||||
buildContext().then((context) => {
|
|
||||||
singletonContext = context;
|
|
||||||
});
|
|
||||||
|
|
||||||
export function useCollectionState(context: IApplicationContext = singletonContext) {
|
|
||||||
const events = new EventSubscriptionCollection();
|
const events = new EventSubscriptionCollection();
|
||||||
const ownEvents = new EventSubscriptionCollection();
|
const ownEvents = new EventSubscriptionCollection();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { IEnvironment } from '@/application/Environment/IEnvironment';
|
||||||
|
|
||||||
|
export function useEnvironment(environment: IEnvironment) {
|
||||||
|
if (!environment) {
|
||||||
|
throw new Error('missing environment');
|
||||||
|
}
|
||||||
|
return environment;
|
||||||
|
}
|
||||||
@@ -18,9 +18,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent, inject } from 'vue';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { useEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||||
import DownloadUrlListItem from './DownloadUrlListItem.vue';
|
import DownloadUrlListItem from './DownloadUrlListItem.vue';
|
||||||
|
|
||||||
const supportedOperativeSystems: readonly OperatingSystem[] = [
|
const supportedOperativeSystems: readonly OperatingSystem[] = [
|
||||||
@@ -34,7 +34,7 @@ export default defineComponent({
|
|||||||
DownloadUrlListItem,
|
DownloadUrlListItem,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const currentOs = Environment.CurrentEnvironment.os;
|
const { os: currentOs } = inject(useEnvironmentKey);
|
||||||
const supportedDesktops = [
|
const supportedDesktops = [
|
||||||
...supportedOperativeSystems,
|
...supportedOperativeSystems,
|
||||||
].sort((os) => (os === currentOs ? 0 : 1));
|
].sort((os) => (os === currentOs ? 0 : 1));
|
||||||
|
|||||||
@@ -12,12 +12,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, PropType, computed,
|
defineComponent, PropType, computed,
|
||||||
|
inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { useApplicationKey, useEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
|
||||||
|
|
||||||
const currentOs = Environment.CurrentEnvironment.os;
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -27,7 +25,8 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { info } = useApplication();
|
const { info } = inject(useApplicationKey);
|
||||||
|
const { os: currentOs } = inject(useEnvironmentKey);
|
||||||
|
|
||||||
const isCurrentOs = computed<boolean>(() => {
|
const isCurrentOs = computed<boolean>(() => {
|
||||||
return currentOs === props.operatingSystem;
|
return currentOs === props.operatingSystem;
|
||||||
|
|||||||
@@ -41,15 +41,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed } from 'vue';
|
import { defineComponent, computed, inject } from 'vue';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { useApplicationKey, useEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
|
||||||
|
|
||||||
const { isDesktop } = Environment.CurrentEnvironment;
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const { info } = useApplication();
|
const { info } = inject(useApplicationKey);
|
||||||
|
const { isDesktop } = inject(useEnvironmentKey);
|
||||||
|
|
||||||
const repositoryUrl = computed<string>(() => info.repositoryUrl);
|
const repositoryUrl = computed<string>(() => info.repositoryUrl);
|
||||||
const feedbackUrl = computed<string>(() => info.feedbackUrl);
|
const feedbackUrl = computed<string>(() => info.feedbackUrl);
|
||||||
|
|||||||
@@ -44,15 +44,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import {
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
defineComponent, ref, computed, inject,
|
||||||
|
} from 'vue';
|
||||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
import { useApplicationKey, useEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||||
import DownloadUrlList from './DownloadUrlList.vue';
|
import DownloadUrlList from './DownloadUrlList.vue';
|
||||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||||
|
|
||||||
const { isDesktop } = Environment.CurrentEnvironment;
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
ModalDialog,
|
ModalDialog,
|
||||||
@@ -60,7 +59,8 @@ export default defineComponent({
|
|||||||
DownloadUrlList,
|
DownloadUrlList,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { info } = useApplication();
|
const { info } = inject(useApplicationKey);
|
||||||
|
const { isDesktop } = inject(useEnvironmentKey);
|
||||||
|
|
||||||
const isPrivacyDialogVisible = ref(false);
|
const isPrivacyDialogVisible = ref(false);
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed } from 'vue';
|
import { defineComponent, computed, inject } from 'vue';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
import { useApplicationKey } from '@/presentation/injectionSymbols';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const { info } = useApplication();
|
const { info } = inject(useApplicationKey);
|
||||||
|
|
||||||
const title = computed(() => info.name);
|
const title = computed(() => info.name);
|
||||||
const subtitle = computed(() => info.slogan);
|
const subtitle = computed(() => info.slogan);
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, ref, watch, computed,
|
defineComponent, ref, watch, computed,
|
||||||
|
inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||||
import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
|
import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
|
||||||
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
@@ -29,7 +30,7 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const {
|
const {
|
||||||
modifyCurrentState, onStateChange, events, currentState,
|
modifyCurrentState, onStateChange, events, currentState,
|
||||||
} = useCollectionState();
|
} = inject(useCollectionStateKey)();
|
||||||
|
|
||||||
const searchPlaceholder = computed<string>(() => {
|
const searchPlaceholder = computed<string>(() => {
|
||||||
const { totalScripts } = currentState.value.collection;
|
const { totalScripts } = currentState.value.collection;
|
||||||
|
|||||||
16
src/presentation/injectionSymbols.ts
Normal file
16
src/presentation/injectionSymbols.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
||||||
|
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
||||||
|
import { useEnvironment } from '@/presentation/components/Shared/Hooks/UseEnvironment';
|
||||||
|
import type { InjectionKey } from 'vue';
|
||||||
|
|
||||||
|
export const useCollectionStateKey = defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState');
|
||||||
|
export const useApplicationKey = defineSingletonKey<ReturnType<typeof useApplication>>('useApplication');
|
||||||
|
export const useEnvironmentKey = defineSingletonKey<ReturnType<typeof useEnvironment>>('useEnvironment');
|
||||||
|
|
||||||
|
function defineSingletonKey<T>(key: string) {
|
||||||
|
return Symbol(key) as InjectionKey<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defineTransientKey<T>(key: string) {
|
||||||
|
return Symbol(key) as InjectionKey<() => T>;
|
||||||
|
}
|
||||||
@@ -3,8 +3,6 @@ import { buildContext } from '@/application/Context/ApplicationContextFactory';
|
|||||||
import App from './components/App.vue';
|
import App from './components/App.vue';
|
||||||
import { ApplicationBootstrapper } from './bootstrapping/ApplicationBootstrapper';
|
import { ApplicationBootstrapper } from './bootstrapping/ApplicationBootstrapper';
|
||||||
|
|
||||||
let vue: Vue;
|
|
||||||
|
|
||||||
buildContext().then(() => {
|
buildContext().then(() => {
|
||||||
// hack workaround to solve running tests through
|
// hack workaround to solve running tests through
|
||||||
// Vue CLI throws 'Top-level-await is only supported in EcmaScript Modules'
|
// Vue CLI throws 'Top-level-await is only supported in EcmaScript Modules'
|
||||||
@@ -12,9 +10,7 @@ buildContext().then(() => {
|
|||||||
new ApplicationBootstrapper()
|
new ApplicationBootstrapper()
|
||||||
.bootstrap(Vue);
|
.bootstrap(Vue);
|
||||||
|
|
||||||
vue = new Vue({
|
new Vue({
|
||||||
render: (h) => h(App),
|
render: (h) => h(App),
|
||||||
}).$mount('#app');
|
}).$mount('#app');
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getVue = () => vue; // exporting is hack until Vue 3 so vue-js-modal can be used
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import 'mocha';
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
|
||||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
|
||||||
|
|
||||||
describe('UseApplication', () => {
|
|
||||||
it('should return the actual application from factory', async () => {
|
|
||||||
// arrange
|
|
||||||
const expected = await ApplicationFactory.Current.getApp();
|
|
||||||
|
|
||||||
// act
|
|
||||||
const { application } = useApplication(expected);
|
|
||||||
|
|
||||||
// assert
|
|
||||||
expect(application).to.equal(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the actual info from the application', async () => {
|
|
||||||
// arrange
|
|
||||||
const app = await ApplicationFactory.Current.getApp();
|
|
||||||
const expected = app.info;
|
|
||||||
|
|
||||||
// act
|
|
||||||
const { info } = useApplication();
|
|
||||||
|
|
||||||
// assert
|
|
||||||
expect(info).to.equal(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import 'mocha';
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
|
||||||
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
|
||||||
|
|
||||||
describe('UseCollectionState', () => {
|
|
||||||
describe('currentContext', () => {
|
|
||||||
it('multiple calls get the same instance', () => {
|
|
||||||
// act
|
|
||||||
const firstContext = useCollectionState().currentContext;
|
|
||||||
const secondContext = useCollectionState().currentContext;
|
|
||||||
// assert
|
|
||||||
expect(firstContext).to.equal(secondContext);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('currentState', () => {
|
|
||||||
it('returns current collection state', () => {
|
|
||||||
// arrange
|
|
||||||
const { currentContext } = useCollectionState();
|
|
||||||
const expectedState = currentContext.state;
|
|
||||||
|
|
||||||
// act
|
|
||||||
const { currentState } = useCollectionState();
|
|
||||||
const actualState = currentState.value;
|
|
||||||
|
|
||||||
// assert
|
|
||||||
expect(expectedState).to.equal(actualState);
|
|
||||||
});
|
|
||||||
it('returns changed collection state', () => {
|
|
||||||
// arrange
|
|
||||||
const { currentContext, currentState, modifyCurrentContext } = useCollectionState();
|
|
||||||
const newOs = pickNonCurrentOs(currentContext);
|
|
||||||
|
|
||||||
// act
|
|
||||||
modifyCurrentContext((context) => {
|
|
||||||
context.changeContext(newOs);
|
|
||||||
});
|
|
||||||
const expectedState = currentContext.state;
|
|
||||||
|
|
||||||
// assert
|
|
||||||
expect(currentState.value).to.equal(expectedState);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('modifyCurrentContext', () => {
|
|
||||||
it('modifies the current context', () => {
|
|
||||||
// arrange
|
|
||||||
const { currentContext, currentState, modifyCurrentContext } = useCollectionState();
|
|
||||||
const expectedOs = pickNonCurrentOs(currentContext);
|
|
||||||
|
|
||||||
// act
|
|
||||||
modifyCurrentContext((context) => {
|
|
||||||
context.changeContext(expectedOs);
|
|
||||||
});
|
|
||||||
|
|
||||||
// assert
|
|
||||||
expect(currentContext.state.os).to.equal(expectedOs);
|
|
||||||
expect(currentState.value.os).to.equal(expectedOs);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('modifyCurrentState', () => {
|
|
||||||
it('modifies the current state', () => {
|
|
||||||
// arrange
|
|
||||||
const { currentState, modifyCurrentState } = useCollectionState();
|
|
||||||
const expectedFilter = 'expected-filter';
|
|
||||||
|
|
||||||
// act
|
|
||||||
modifyCurrentState((state) => {
|
|
||||||
state.filter.setFilter(expectedFilter);
|
|
||||||
});
|
|
||||||
|
|
||||||
// assert
|
|
||||||
const actualFilter = currentState.value.filter.currentFilter.query;
|
|
||||||
expect(actualFilter).to.equal(expectedFilter);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function pickNonCurrentOs(context: IReadOnlyApplicationContext) {
|
|
||||||
return context.app.getSupportedOsList().find((os) => os !== context.state.os);
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,21 @@ import { expect } from 'chai';
|
|||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
||||||
import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub';
|
import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub';
|
||||||
import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformationStub';
|
import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformationStub';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
describe('UseApplication', () => {
|
describe('UseApplication', () => {
|
||||||
|
describe('application is absent', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing application';
|
||||||
|
const applicationValue = absentValue;
|
||||||
|
// act
|
||||||
|
const act = () => useApplication(applicationValue);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should return expected info', () => {
|
it('should return expected info', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedInfo = new ProjectInformationStub()
|
const expectedInfo = new ProjectInformationStub()
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
|||||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
describe('UseCollectionState', () => {
|
describe('UseCollectionState', () => {
|
||||||
|
describe('context is absent', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing context';
|
||||||
|
const contextValue = absentValue;
|
||||||
|
// act
|
||||||
|
const act = () => useCollectionState(contextValue);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('currentContext', () => {
|
describe('currentContext', () => {
|
||||||
it('returns current context', () => {
|
it('returns current context', () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { useEnvironment } from '@/presentation/components/Shared/Hooks/UseEnvironment';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import { EnvironmentStub } from '@tests/unit/shared/Stubs/EnvironmentStub';
|
||||||
|
|
||||||
|
describe('UseEnvironment', () => {
|
||||||
|
describe('environment is absent', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'missing environment';
|
||||||
|
const environmentValue = absentValue;
|
||||||
|
// act
|
||||||
|
const act = () => useEnvironment(environmentValue);
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns expected environment', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedEnvironment = new EnvironmentStub();
|
||||||
|
// act
|
||||||
|
const actualEnvironment = useEnvironment(expectedEnvironment);
|
||||||
|
// assert
|
||||||
|
expect(actualEnvironment).to.equal(expectedEnvironment);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user