Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af7219f6e1 | ||
|
|
8ccaec7af6 | ||
|
|
b2ffc90da7 | ||
|
|
72e4d0b896 |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,5 +1,30 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.12.6 (2023-11-03)
|
||||||
|
|
||||||
|
* Bump dependencies to latest | [25d7f7b](https://github.com/undergroundwires/privacy.sexy/commit/25d7f7b2a479e51e092881cc2751e67a7d3f179f)
|
||||||
|
* win: improve system app uninstall cleanup #73 | [dbe3c5c](https://github.com/undergroundwires/privacy.sexy/commit/dbe3c5cfb91ba8a1657838b69117858843c8fbc8)
|
||||||
|
* win: improve system app uninstall /w fallback #260 | [98a26f9](https://github.com/undergroundwires/privacy.sexy/commit/98a26f9ae47af2668aa53f39d1768983036048ce)
|
||||||
|
* Improve performance of rendering during search | [79b46bf](https://github.com/undergroundwires/privacy.sexy/commit/79b46bf21004d96d31551439e5db5d698a3f71f3)
|
||||||
|
* Fix YAML error for site release in CI/CD | [237d994](https://github.com/undergroundwires/privacy.sexy/commit/237d9944f900f5172366868d75219224ff0542b0)
|
||||||
|
* win: fix Microsoft Advertising app removal #200 | [e40b9a3](https://github.com/undergroundwires/privacy.sexy/commit/e40b9a3cf53c341f2e84023a9f0e9680ac08f3fa)
|
||||||
|
* win: improve directory cleanup security | [060e789](https://github.com/undergroundwires/privacy.sexy/commit/060e7896624309aebd25e8b190c127282de177e8)
|
||||||
|
* Centralize Electron entry file path configuration | [d6da406](https://github.com/undergroundwires/privacy.sexy/commit/d6da406c61e5b9f5408851d1302d6d7398157a2e)
|
||||||
|
* win: prevent updates from reinstalling apps #260 | [8570b02](https://github.com/undergroundwires/privacy.sexy/commit/8570b02dde14ffad64863f614682c3fc1f87b6c2)
|
||||||
|
* win: improve script environment robustness #221 | [dfd4451](https://github.com/undergroundwires/privacy.sexy/commit/dfd44515613f38abe5a806bda36f44e7b715b50b)
|
||||||
|
* Fix compiler failing with nested `with` expression | [80821fc](https://github.com/undergroundwires/privacy.sexy/commit/80821fca0769e5fd2c6338918fbdcea12fbe83d2)
|
||||||
|
* win: improve soft file/app delete security #260 | [f4a74f0](https://github.com/undergroundwires/privacy.sexy/commit/f4a74f058db9b5bcbcbe438785db5ec88ecc1657)
|
||||||
|
* Fix incorrect tooltip position after window resize | [f8e5f1a](https://github.com/undergroundwires/privacy.sexy/commit/f8e5f1a5a2afa1f18567e6d965359b6a1f082367)
|
||||||
|
* linux: fix string formatting of Firefox configs | [e775d68](https://github.com/undergroundwires/privacy.sexy/commit/e775d68a9b4a5f9e893ff0e3500dade036185193)
|
||||||
|
* win: improve file delete | [e72c1c1](https://github.com/undergroundwires/privacy.sexy/commit/e72c1c13ea2d73ebfc7a8da5a21254fdfc0e5b59)
|
||||||
|
* win: change system app removal to hard delete #260 | [77123d8](https://github.com/undergroundwires/privacy.sexy/commit/77123d8c929d23676a9cb21d7b697703fd1b6e82)
|
||||||
|
* Improve UI performance by optimizing reactivity | [4995e49](https://github.com/undergroundwires/privacy.sexy/commit/4995e49c469211404dac9fcb79b75eb121f80bce)
|
||||||
|
* Migrate to Vue 3.0 #230 | [ca81f68](https://github.com/undergroundwires/privacy.sexy/commit/ca81f68ff1c3bbe5b22981096ae9220b0b5851c7)
|
||||||
|
* win, linux: unify & improve Firefox clean-up #273 | [0466b86](https://github.com/undergroundwires/privacy.sexy/commit/0466b86f1013341c966a9bbf6513990337b16598)
|
||||||
|
* win: fix store revert for multiple installs #260 | [5bb13e3](https://github.com/undergroundwires/privacy.sexy/commit/5bb13e34f8de2e2a7ba943ff72b12c0569435e62)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.5...0.12.6)
|
||||||
|
|
||||||
## 0.12.5 (2023-10-13)
|
## 0.12.5 (2023-10-13)
|
||||||
|
|
||||||
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)
|
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -122,7 +122,7 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-Setup-0.12.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-0.12.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-0.12.5.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.6/privacy.sexy-Setup-0.12.6.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.6/privacy.sexy-0.12.6.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.6/privacy.sexy-0.12.6.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
||||||
|
|
||||||
@@ -153,12 +153,21 @@ Online version does not require to run any software on your computer. Offline ve
|
|||||||
## Additional Install Options
|
## Additional Install Options
|
||||||
|
|
||||||
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions.
|
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions.
|
||||||
- Using [Scoop](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) package manager on Windows:
|
- Other unofficial channels (not maintained by privacy.sexy) for Windows include:
|
||||||
|
- [Scoop 🥄](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) (latest version):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
scoop bucket add extras
|
scoop bucket add extras
|
||||||
scoop install privacy.sexy
|
scoop install privacy.sexy
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- [winget 🪟](https://winget.run/pkg/undergroundwires/privacy.sexy) (may be outdated):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget install -e --id undergroundwires.privacy.sexy
|
||||||
|
```
|
||||||
|
|
||||||
|
With winget, updates require manual submission; the auto-update feature within privacy.sexy will notify you of new releases post-installation.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.5",
|
"version": "0.12.6",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.5",
|
"version": "0.12.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Now you have the choice",
|
"slogan": "Now you have the choice",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
export class Clipboard {
|
|
||||||
public static copyText(text: string): void {
|
|
||||||
const el = document.createElement('textarea');
|
|
||||||
el.value = text;
|
|
||||||
el.setAttribute('readonly', ''); // to avoid focus
|
|
||||||
el.style.position = 'absolute';
|
|
||||||
el.style.left = '-9999px';
|
|
||||||
document.body.appendChild(el);
|
|
||||||
el.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,8 @@ import { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hook
|
|||||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
|
import { useClipboard } from '../components/Shared/Hooks/Clipboard/UseClipboard';
|
||||||
|
import { useCurrentCode } from '../components/Shared/Hooks/UseCurrentCode';
|
||||||
|
|
||||||
export function provideDependencies(
|
export function provideDependencies(
|
||||||
context: IApplicationContext,
|
context: IApplicationContext,
|
||||||
@@ -23,6 +25,12 @@ export function provideDependencies(
|
|||||||
const { events } = api.inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
const { events } = api.inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
return useCollectionState(context, events);
|
return useCollectionState(context, events);
|
||||||
});
|
});
|
||||||
|
registerTransient(InjectionKeys.useClipboard, () => useClipboard());
|
||||||
|
registerTransient(InjectionKeys.useCurrentCode, () => {
|
||||||
|
const { events } = api.inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||||
|
const state = api.inject(InjectionKeys.useCollectionState)();
|
||||||
|
return useCurrentCode(state, events);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VueDependencyInjectionApi {
|
export interface VueDependencyInjectionApi {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<IconButton
|
||||||
|
text="Copy"
|
||||||
|
@click="copyCode"
|
||||||
|
icon-name="copy"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
defineComponent, inject,
|
||||||
|
} from 'vue';
|
||||||
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
|
import IconButton from './IconButton.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
IconButton,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const { copyText } = inject(InjectionKeys.useClipboard)();
|
||||||
|
const { currentCode } = inject(InjectionKeys.useCurrentCode)();
|
||||||
|
|
||||||
|
async function copyCode() {
|
||||||
|
await copyText(currentCode.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
copyCode,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<IconButton
|
||||||
|
v-if="canRun"
|
||||||
|
text="Run"
|
||||||
|
@click="executeCode"
|
||||||
|
icon-name="play"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
defineComponent, computed, inject,
|
||||||
|
} from 'vue';
|
||||||
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { CodeRunner } from '@/infrastructure/CodeRunner';
|
||||||
|
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
|
import IconButton from './IconButton.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
IconButton,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const { currentState, currentContext } = inject(InjectionKeys.useCollectionState)();
|
||||||
|
const { os, isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||||
|
|
||||||
|
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop, os));
|
||||||
|
|
||||||
|
async function executeCode() {
|
||||||
|
await runCode(currentContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDesktopVersion: isDesktop,
|
||||||
|
canRun,
|
||||||
|
executeCode,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCanRunState(
|
||||||
|
selectedOs: OperatingSystem,
|
||||||
|
isDesktopVersion: boolean,
|
||||||
|
hostOs: OperatingSystem,
|
||||||
|
): boolean {
|
||||||
|
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||||
|
return isDesktopVersion && isRunningOnSelectedOs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCode(context: IReadOnlyApplicationContext) {
|
||||||
|
const runner = new CodeRunner();
|
||||||
|
await runner.runCode(
|
||||||
|
/* code: */ context.state.code.current,
|
||||||
|
/* appName: */ context.app.info.name,
|
||||||
|
/* fileExtension: */ context.state.collection.scripting.fileExtension,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<div class="button-wrapper">
|
||||||
class="button"
|
<button
|
||||||
type="button"
|
class="button"
|
||||||
@click="onClicked"
|
type="button"
|
||||||
>
|
@click="onClicked"
|
||||||
<AppIcon
|
>
|
||||||
class="button__icon"
|
<AppIcon
|
||||||
:icon="iconName"
|
class="button__icon"
|
||||||
/>
|
:icon="iconName"
|
||||||
<div class="button__text">{{text}}</div>
|
/>
|
||||||
</button>
|
<div class="button__text">{{text}}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -49,10 +51,20 @@ export default defineComponent({
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@use "@/presentation/assets/styles/main" as *;
|
@use "@/presentation/assets/styles/main" as *;
|
||||||
|
|
||||||
|
.button-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 70px;
|
||||||
|
.button {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
background-color: $color-secondary;
|
background-color: $color-secondary;
|
||||||
color: $color-on-secondary;
|
color: $color-on-secondary;
|
||||||
@@ -70,19 +82,17 @@ export default defineComponent({
|
|||||||
|
|
||||||
@include clickable;
|
@include clickable;
|
||||||
|
|
||||||
width: 10%;
|
|
||||||
min-width: 90px;
|
|
||||||
@include hover-or-touch {
|
@include hover-or-touch {
|
||||||
background: $color-surface;
|
background: $color-surface;
|
||||||
box-shadow: 0px 2px 10px 5px $color-secondary;
|
box-shadow: 0px 2px 10px 5px $color-secondary;
|
||||||
|
.button__text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.button__icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@include hover-or-touch('>&__text') {
|
.button__text {
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
@include hover-or-touch('>&__icon') {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&__text {
|
|
||||||
display: none;
|
display: none;
|
||||||
font-family: $font-artistic;
|
font-family: $font-artistic;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
:text="isDesktopVersion ? 'Save' : 'Download'"
|
||||||
|
@click="saveCode"
|
||||||
|
:icon-name="isDesktopVersion ? 'floppy-disk' : 'file-arrow-down'"
|
||||||
|
/>
|
||||||
|
<ModalDialog v-if="instructions" v-model="areInstructionsVisible">
|
||||||
|
<InstructionList :data="instructions" />
|
||||||
|
</ModalDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
defineComponent, ref, computed, inject,
|
||||||
|
} from 'vue';
|
||||||
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
|
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||||
|
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||||
|
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import IconButton from '../IconButton.vue';
|
||||||
|
import InstructionList from './Instructions/InstructionList.vue';
|
||||||
|
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||||
|
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
IconButton,
|
||||||
|
InstructionList,
|
||||||
|
ModalDialog,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const { currentState } = inject(InjectionKeys.useCollectionState)();
|
||||||
|
const { isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||||
|
|
||||||
|
const areInstructionsVisible = ref(false);
|
||||||
|
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||||
|
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
||||||
|
currentState.value.collection.os,
|
||||||
|
fileName.value,
|
||||||
|
));
|
||||||
|
|
||||||
|
function saveCode() {
|
||||||
|
saveCodeToDisk(fileName.value, currentState.value);
|
||||||
|
areInstructionsVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDesktopVersion: isDesktop,
|
||||||
|
instructions,
|
||||||
|
fileName,
|
||||||
|
areInstructionsVisible,
|
||||||
|
saveCode,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function getDownloadInstructions(
|
||||||
|
os: OperatingSystem,
|
||||||
|
fileName: string,
|
||||||
|
): IInstructionListData | undefined {
|
||||||
|
if (!hasInstructions(os)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return getInstructions(os, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
||||||
|
const content = state.code.current;
|
||||||
|
const type = getType(state.collection.scripting.language);
|
||||||
|
SaveFileDialog.saveFile(content, fileName, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getType(language: ScriptingLanguage) {
|
||||||
|
switch (language) {
|
||||||
|
case ScriptingLanguage.batchfile:
|
||||||
|
return FileType.BatchFile;
|
||||||
|
case ScriptingLanguage.shellscript:
|
||||||
|
return FileType.ShellScript;
|
||||||
|
default:
|
||||||
|
throw new Error('unknown file type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function buildFileName(scripting: IScriptingDefinition) {
|
||||||
|
const fileName = 'privacy-script';
|
||||||
|
if (scripting.fileExtension) {
|
||||||
|
return `${fileName}.${scripting.fileExtension}`;
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, shallowRef } from 'vue';
|
import { defineComponent, shallowRef, inject } from 'vue';
|
||||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
|
||||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||||
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -27,9 +27,11 @@ export default defineComponent({
|
|||||||
AppIcon,
|
AppIcon,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
|
const { copyText } = inject(InjectionKeys.useClipboard)();
|
||||||
|
|
||||||
const codeElement = shallowRef<HTMLElement | undefined>();
|
const codeElement = shallowRef<HTMLElement | undefined>();
|
||||||
|
|
||||||
function copyCode() {
|
async function copyCode() {
|
||||||
const element = codeElement.value;
|
const element = codeElement.value;
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Code element could not be found.');
|
throw new Error('Code element could not be found.');
|
||||||
@@ -38,7 +40,7 @@ export default defineComponent({
|
|||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('Code element does not contain any text.');
|
throw new Error('Code element does not contain any text.');
|
||||||
}
|
}
|
||||||
Clipboard.copyText(code);
|
await copyText(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1,168 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container" v-if="hasCode">
|
<div class="container" v-if="hasCode">
|
||||||
<IconButton
|
<CodeRunButton class="code-button" />
|
||||||
v-if="canRun"
|
<CodeSaveButton class="code-button" />
|
||||||
text="Run"
|
<CodeCopyButton class="code-button" />
|
||||||
v-on:click="executeCode"
|
|
||||||
icon-name="play"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
:text="isDesktopVersion ? 'Save' : 'Download'"
|
|
||||||
v-on:click="saveCode"
|
|
||||||
:icon-name="isDesktopVersion ? 'floppy-disk' : 'file-arrow-down'"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
text="Copy"
|
|
||||||
v-on:click="copyCode"
|
|
||||||
icon-name="copy"
|
|
||||||
/>
|
|
||||||
<ModalDialog v-if="instructions" v-model="areInstructionsVisible">
|
|
||||||
<InstructionList :data="instructions" />
|
|
||||||
</ModalDialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent, ref, computed, inject,
|
defineComponent, computed, inject,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
import CodeRunButton from './CodeRunButton.vue';
|
||||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
import CodeCopyButton from './CodeCopyButton.vue';
|
||||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
import CodeSaveButton from './Save/CodeSaveButton.vue';
|
||||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import { CodeRunner } from '@/infrastructure/CodeRunner';
|
|
||||||
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
|
||||||
import InstructionList from './Instructions/InstructionList.vue';
|
|
||||||
import IconButton from './IconButton.vue';
|
|
||||||
import { IInstructionListData } from './Instructions/InstructionListData';
|
|
||||||
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
IconButton,
|
CodeRunButton,
|
||||||
InstructionList,
|
CodeCopyButton,
|
||||||
ModalDialog,
|
CodeSaveButton,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const {
|
const { currentCode } = inject(InjectionKeys.useCurrentCode)();
|
||||||
currentState, currentContext, onStateChange,
|
|
||||||
} = inject(InjectionKeys.useCollectionState)();
|
|
||||||
const { os, isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
|
||||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
|
||||||
|
|
||||||
const areInstructionsVisible = ref(false);
|
const hasCode = computed<boolean>(() => currentCode.value.length > 0);
|
||||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop, os));
|
|
||||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
|
||||||
const hasCode = ref(false);
|
|
||||||
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
|
||||||
currentState.value.collection.os,
|
|
||||||
fileName.value,
|
|
||||||
));
|
|
||||||
|
|
||||||
async function copyCode() {
|
|
||||||
const code = await getCurrentCode();
|
|
||||||
Clipboard.copyText(code.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveCode() {
|
|
||||||
saveCodeToDisk(fileName.value, currentState.value);
|
|
||||||
areInstructionsVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeCode() {
|
|
||||||
await runCode(currentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
onStateChange((newState) => {
|
|
||||||
updateCurrentCode(newState.code.current);
|
|
||||||
subscribeToCodeChanges(newState.code);
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
function subscribeToCodeChanges(code: IApplicationCode) {
|
|
||||||
events.unsubscribeAllAndRegister([
|
|
||||||
code.changed.on((newCode) => updateCurrentCode(newCode.code)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCurrentCode(code: string) {
|
|
||||||
hasCode.value = code && code.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCurrentCode(): Promise<IApplicationCode> {
|
|
||||||
const { code } = currentContext.state;
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDesktopVersion: isDesktop,
|
|
||||||
canRun,
|
|
||||||
hasCode,
|
hasCode,
|
||||||
instructions,
|
|
||||||
fileName,
|
|
||||||
areInstructionsVisible,
|
|
||||||
copyCode,
|
|
||||||
saveCode,
|
|
||||||
executeCode,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function getDownloadInstructions(
|
|
||||||
os: OperatingSystem,
|
|
||||||
fileName: string,
|
|
||||||
): IInstructionListData | undefined {
|
|
||||||
if (!hasInstructions(os)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return getInstructions(os, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCanRunState(
|
|
||||||
selectedOs: OperatingSystem,
|
|
||||||
isDesktopVersion: boolean,
|
|
||||||
hostOs: OperatingSystem,
|
|
||||||
): boolean {
|
|
||||||
const isRunningOnSelectedOs = selectedOs === hostOs;
|
|
||||||
return isDesktopVersion && isRunningOnSelectedOs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
|
||||||
const content = state.code.current;
|
|
||||||
const type = getType(state.collection.scripting.language);
|
|
||||||
SaveFileDialog.saveFile(content, fileName, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getType(language: ScriptingLanguage) {
|
|
||||||
switch (language) {
|
|
||||||
case ScriptingLanguage.batchfile:
|
|
||||||
return FileType.BatchFile;
|
|
||||||
case ScriptingLanguage.shellscript:
|
|
||||||
return FileType.ShellScript;
|
|
||||||
default:
|
|
||||||
throw new Error('unknown file type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function buildFileName(scripting: IScriptingDefinition) {
|
|
||||||
const fileName = 'privacy-script';
|
|
||||||
if (scripting.fileExtension) {
|
|
||||||
return `${fileName}.${scripting.fileExtension}`;
|
|
||||||
}
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runCode(context: IReadOnlyApplicationContext) {
|
|
||||||
const runner = new CodeRunner();
|
|
||||||
await runner.runCode(
|
|
||||||
/* code: */ context.state.code.current,
|
|
||||||
/* appName: */ context.app.info.name,
|
|
||||||
/* fileExtension: */ context.state.collection.scripting.fileExtension,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -170,8 +38,10 @@ async function runCode(context: IReadOnlyApplicationContext) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 30px;
|
||||||
}
|
}
|
||||||
.container > * + * {
|
.code-button {
|
||||||
margin-left: 30px;
|
width: 10%;
|
||||||
|
min-width: 90px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
computed, inject, shallowReadonly, shallowRef,
|
computed, inject, shallowReadonly, shallowRef, triggerRef,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
@@ -25,11 +25,21 @@ function useSelectedScripts() {
|
|||||||
|
|
||||||
const selectedScripts = shallowRef<readonly SelectedScript[]>([]);
|
const selectedScripts = shallowRef<readonly SelectedScript[]>([]);
|
||||||
|
|
||||||
|
function updateSelectedScripts(newReference: readonly SelectedScript[]) {
|
||||||
|
if (selectedScripts.value === newReference) {
|
||||||
|
// Manually trigger update if the array was mutated using the same reference.
|
||||||
|
// Array might have been mutated without changing the reference
|
||||||
|
triggerRef(selectedScripts);
|
||||||
|
} else {
|
||||||
|
selectedScripts.value = newReference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onStateChange((state) => {
|
onStateChange((state) => {
|
||||||
selectedScripts.value = state.selection.selectedScripts;
|
updateSelectedScripts(state.selection.selectedScripts);
|
||||||
events.unsubscribeAllAndRegister([
|
events.unsubscribeAllAndRegister([
|
||||||
state.selection.changed.on((scripts) => {
|
state.selection.changed.on((scripts) => {
|
||||||
selectedScripts.value = scripts;
|
updateSelectedScripts(scripts);
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Clipboard } from './Clipboard';
|
||||||
|
|
||||||
|
export type NavigatorClipboard = typeof globalThis.navigator.clipboard;
|
||||||
|
|
||||||
|
export class BrowserClipboard implements Clipboard {
|
||||||
|
constructor(
|
||||||
|
private readonly navigatorClipboard: NavigatorClipboard = globalThis.navigator.clipboard,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async copyText(text: string): Promise<void> {
|
||||||
|
await this.navigatorClipboard.writeText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface Clipboard {
|
||||||
|
copyText(text: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { FunctionKeys } from '@/TypeHelpers';
|
||||||
|
import { BrowserClipboard } from './BrowserClipboard';
|
||||||
|
import { Clipboard } from './Clipboard';
|
||||||
|
|
||||||
|
export function useClipboard(clipboard: Clipboard = new BrowserClipboard()) {
|
||||||
|
// Bind functions for direct use from destructured assignments such as `const { .. } = ...`.
|
||||||
|
const functionKeys: readonly FunctionKeys<Clipboard>[] = ['copyText'];
|
||||||
|
functionKeys.forEach((functionName) => {
|
||||||
|
const fn = clipboard[functionName];
|
||||||
|
clipboard[functionName] = fn.bind(clipboard);
|
||||||
|
});
|
||||||
|
return clipboard;
|
||||||
|
}
|
||||||
32
src/presentation/components/Shared/Hooks/UseCurrentCode.ts
Normal file
32
src/presentation/components/Shared/Hooks/UseCurrentCode.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
|
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
||||||
|
import { useCollectionState } from './UseCollectionState';
|
||||||
|
|
||||||
|
export function useCurrentCode(
|
||||||
|
state: ReturnType<typeof useCollectionState>,
|
||||||
|
events: IEventSubscriptionCollection,
|
||||||
|
) {
|
||||||
|
const { onStateChange } = state;
|
||||||
|
|
||||||
|
const currentCode = ref<string>('');
|
||||||
|
|
||||||
|
onStateChange((newState) => {
|
||||||
|
updateCurrentCode(newState.code.current);
|
||||||
|
subscribeToCodeChanges(newState.code);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
function subscribeToCodeChanges(code: IApplicationCode) {
|
||||||
|
events.unsubscribeAllAndRegister([
|
||||||
|
code.changed.on((newCode) => updateCurrentCode(newCode.code)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentCode(newCode: string) {
|
||||||
|
currentCode.value = newCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentCode,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-html="svgContent" class="inline-icon" />
|
<div
|
||||||
|
class="inline-icon"
|
||||||
|
v-html="svgContent"
|
||||||
|
@click="onClicked"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -18,10 +22,19 @@ export default defineComponent({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
emits: [
|
||||||
|
'click',
|
||||||
|
],
|
||||||
|
setup(props, { emit }) {
|
||||||
const useSvgLoaderHook = inject('useSvgLoaderHook', useSvgLoader);
|
const useSvgLoaderHook = inject('useSvgLoaderHook', useSvgLoader);
|
||||||
|
|
||||||
const { svgContent } = useSvgLoaderHook(() => props.icon);
|
const { svgContent } = useSvgLoaderHook(() => props.icon);
|
||||||
return { svgContent };
|
|
||||||
|
function onClicked() {
|
||||||
|
emit('click');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { svgContent, onClicked };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
import type { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
||||||
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
import type { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
|
||||||
import { useRuntimeEnvironment } from '@/presentation/components/Shared/Hooks/UseRuntimeEnvironment';
|
import type { useRuntimeEnvironment } from '@/presentation/components/Shared/Hooks/UseRuntimeEnvironment';
|
||||||
import type { useAutoUnsubscribedEvents } from './components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
import type { useClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/UseClipboard';
|
||||||
|
import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode';
|
||||||
|
import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
||||||
import type { InjectionKey } from 'vue';
|
import type { InjectionKey } from 'vue';
|
||||||
|
|
||||||
export const InjectionKeys = {
|
export const InjectionKeys = {
|
||||||
@@ -9,6 +11,8 @@ export const InjectionKeys = {
|
|||||||
useApplication: defineSingletonKey<ReturnType<typeof useApplication>>('useApplication'),
|
useApplication: defineSingletonKey<ReturnType<typeof useApplication>>('useApplication'),
|
||||||
useRuntimeEnvironment: defineSingletonKey<ReturnType<typeof useRuntimeEnvironment>>('useRuntimeEnvironment'),
|
useRuntimeEnvironment: defineSingletonKey<ReturnType<typeof useRuntimeEnvironment>>('useRuntimeEnvironment'),
|
||||||
useAutoUnsubscribedEvents: defineTransientKey<ReturnType<typeof useAutoUnsubscribedEvents>>('useAutoUnsubscribedEvents'),
|
useAutoUnsubscribedEvents: defineTransientKey<ReturnType<typeof useAutoUnsubscribedEvents>>('useAutoUnsubscribedEvents'),
|
||||||
|
useClipboard: defineTransientKey<ReturnType<typeof useClipboard>>('useClipboard'),
|
||||||
|
useCurrentCode: defineTransientKey<ReturnType<typeof useCurrentCode>>('useCurrentCode'),
|
||||||
};
|
};
|
||||||
|
|
||||||
function defineSingletonKey<T>(key: string): InjectionKey<T> {
|
function defineSingletonKey<T>(key: string): InjectionKey<T> {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ describe('DependencyProvider', () => {
|
|||||||
useApplication: createSingletonTests(),
|
useApplication: createSingletonTests(),
|
||||||
useRuntimeEnvironment: createSingletonTests(),
|
useRuntimeEnvironment: createSingletonTests(),
|
||||||
useAutoUnsubscribedEvents: createTransientTests(),
|
useAutoUnsubscribedEvents: createTransientTests(),
|
||||||
|
useClipboard: createTransientTests(),
|
||||||
|
useCurrentCode: createTransientTests(),
|
||||||
};
|
};
|
||||||
Object.entries(testCases).forEach(([key, runTests]) => {
|
Object.entries(testCases).forEach(([key, runTests]) => {
|
||||||
describe(`Key: "${key}"`, () => {
|
describe(`Key: "${key}"`, () => {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
|
import CodeCopyButton from '@/presentation/components/Code/CodeButtons/CodeCopyButton.vue';
|
||||||
|
import { ClipboardStub } from '@tests/unit/shared/Stubs/ClipboardStub';
|
||||||
|
import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard';
|
||||||
|
import { UseClipboardStub } from '@tests/unit/shared/Stubs/UseClipboardStub';
|
||||||
|
import { UseCurrentCodeStub } from '@tests/unit/shared/Stubs/UseCurrentCodeStub';
|
||||||
|
|
||||||
|
const COMPONENT_ICON_BUTTON_WRAPPER_NAME = 'IconButton';
|
||||||
|
|
||||||
|
describe('CodeCopyButton', () => {
|
||||||
|
it('copies current code when clicked', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCode = 'code to be copied';
|
||||||
|
const clipboard = new ClipboardStub();
|
||||||
|
const wrapper = mountComponent({
|
||||||
|
clipboard,
|
||||||
|
currentCode: expectedCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
// act
|
||||||
|
await wrapper.trigger('click');
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const calls = clipboard.callHistory;
|
||||||
|
expect(calls).to.have.lengthOf(1);
|
||||||
|
const call = calls.find((c) => c.methodName === 'copyText');
|
||||||
|
expect(call).toBeDefined();
|
||||||
|
const [copiedText] = call.args;
|
||||||
|
expect(copiedText).to.equal(expectedCode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mountComponent(options?: {
|
||||||
|
clipboard?: Clipboard,
|
||||||
|
currentCode?: string,
|
||||||
|
}) {
|
||||||
|
return shallowMount(CodeCopyButton, {
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
[InjectionKeys.useClipboard as symbol]: () => (
|
||||||
|
options?.clipboard
|
||||||
|
? new UseClipboardStub(options.clipboard)
|
||||||
|
: new UseClipboardStub()
|
||||||
|
).get(),
|
||||||
|
[InjectionKeys.useCurrentCode as symbol]: () => (
|
||||||
|
options.currentCode === undefined
|
||||||
|
? new UseCurrentCodeStub()
|
||||||
|
: new UseCurrentCodeStub().withCurrentCode(options.currentCode)
|
||||||
|
).get(),
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
[COMPONENT_ICON_BUTTON_WRAPPER_NAME]: {
|
||||||
|
name: COMPONENT_ICON_BUTTON_WRAPPER_NAME,
|
||||||
|
template: '<div @click="handleClick()" />',
|
||||||
|
emits: ['click'],
|
||||||
|
setup: (_, { emit }) => ({
|
||||||
|
handleClick: () => emit('click'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import CodeInstruction from '@/presentation/components/Code/CodeButtons/Save/Instructions/CodeInstruction.vue';
|
||||||
|
import { expectThrowsAsync } from '@tests/unit/shared/Assertions/ExpectThrowsAsync';
|
||||||
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
|
import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard';
|
||||||
|
import { UseClipboardStub } from '@tests/unit/shared/Stubs/UseClipboardStub';
|
||||||
|
import { ClipboardStub } from '@tests/unit/shared/Stubs/ClipboardStub';
|
||||||
|
|
||||||
|
const DOM_SELECTOR_CODE_SLOT = 'code';
|
||||||
|
const DOM_SELECTOR_COPY_BUTTON = '.copy-button';
|
||||||
|
const COMPONENT_TOOLTIP_WRAPPER_NAME = 'TooltipWrapper';
|
||||||
|
|
||||||
|
describe('CodeInstruction.vue', () => {
|
||||||
|
it('renders a slot content inside a <code> element', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedSlotContent = 'Example Code';
|
||||||
|
const wrapper = mountComponent({
|
||||||
|
slotContent: expectedSlotContent,
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
const codeSlot = wrapper.find(DOM_SELECTOR_CODE_SLOT);
|
||||||
|
const actualContent = codeSlot.text();
|
||||||
|
// assert
|
||||||
|
expect(actualContent).to.equal(expectedSlotContent);
|
||||||
|
});
|
||||||
|
describe('copy', () => {
|
||||||
|
it('calls copyText when the copy button is clicked', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCode = 'Code to be copied';
|
||||||
|
const clipboardStub = new ClipboardStub();
|
||||||
|
const wrapper = mountComponent({
|
||||||
|
clipboard: clipboardStub,
|
||||||
|
});
|
||||||
|
wrapper.vm.codeElement = { textContent: expectedCode } as HTMLElement;
|
||||||
|
// act
|
||||||
|
const copyButton = wrapper.find(DOM_SELECTOR_COPY_BUTTON);
|
||||||
|
await copyButton.trigger('click');
|
||||||
|
// assert
|
||||||
|
const calls = clipboardStub.callHistory;
|
||||||
|
expect(calls).to.have.lengthOf(1);
|
||||||
|
const call = calls.find((c) => c.methodName === 'copyText');
|
||||||
|
expect(call).toBeDefined();
|
||||||
|
const [actualCode] = call.args;
|
||||||
|
expect(actualCode).to.equal(expectedCode);
|
||||||
|
});
|
||||||
|
it('throws an error when codeElement is not found during copy', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'Code element could not be found.';
|
||||||
|
const wrapper = mountComponent();
|
||||||
|
wrapper.vm.codeElement = undefined;
|
||||||
|
// act
|
||||||
|
const act = () => wrapper.vm.copyCode();
|
||||||
|
// assert
|
||||||
|
await expectThrowsAsync(act, expectedError);
|
||||||
|
});
|
||||||
|
it('throws an error when codeElement has no textContent during copy', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'Code element does not contain any text.';
|
||||||
|
const wrapper = mountComponent();
|
||||||
|
wrapper.vm.codeElement = { textContent: '' } as HTMLElement;
|
||||||
|
// act
|
||||||
|
const act = () => wrapper.vm.copyCode();
|
||||||
|
// assert
|
||||||
|
await expectThrowsAsync(act, expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mountComponent(options?: {
|
||||||
|
readonly clipboard?: Clipboard,
|
||||||
|
readonly slotContent?: string,
|
||||||
|
}) {
|
||||||
|
return shallowMount(CodeInstruction, {
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
[InjectionKeys.useClipboard as symbol]:
|
||||||
|
() => {
|
||||||
|
if (options?.clipboard) {
|
||||||
|
return new UseClipboardStub(options.clipboard).get();
|
||||||
|
}
|
||||||
|
return new UseClipboardStub().get();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
[COMPONENT_TOOLTIP_WRAPPER_NAME]: {
|
||||||
|
name: COMPONENT_TOOLTIP_WRAPPER_NAME,
|
||||||
|
template: '<slot />',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
default: options?.slotContent ?? 'Stubbed slot content',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IInstructionsBuilderData, InstructionsBuilder, InstructionStepBuilderType } from '@/presentation/components/Code/CodeButtons/Instructions/Data/InstructionsBuilder';
|
import { IInstructionsBuilderData, InstructionsBuilder, InstructionStepBuilderType } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
|
||||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { IInstructionInfo, IInstructionListStep } from '@/presentation/components/Code/CodeButtons/Instructions/InstructionListData';
|
import { IInstructionInfo, IInstructionListStep } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListData';
|
||||||
|
|
||||||
describe('InstructionsBuilder', () => {
|
describe('InstructionsBuilder', () => {
|
||||||
describe('withStep', () => {
|
describe('withStep', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe } from 'vitest';
|
import { describe } from 'vitest';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { MacOsInstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Instructions/Data/MacOsInstructionsBuilder';
|
import { MacOsInstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/MacOsInstructionsBuilder';
|
||||||
import { runOsSpecificInstructionBuilderTests } from './OsSpecificInstructionBuilderTestRunner';
|
import { runOsSpecificInstructionBuilderTests } from './OsSpecificInstructionBuilderTestRunner';
|
||||||
|
|
||||||
describe('MacOsInstructionsBuilder', () => {
|
describe('MacOsInstructionsBuilder', () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { it, expect } from 'vitest';
|
import { it, expect } from 'vitest';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Instructions/Data/InstructionsBuilder';
|
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
|
||||||
|
|
||||||
interface ITestData {
|
interface ITestData {
|
||||||
readonly factory: () => InstructionsBuilder;
|
readonly factory: () => InstructionsBuilder;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { getInstructions, hasInstructions } from '@/presentation/components/Code/CodeButtons/Instructions/InstructionListDataFactory';
|
import { getInstructions, hasInstructions } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListDataFactory';
|
||||||
import { getEnumValues } from '@/application/Common/Enum';
|
import { getEnumValues } from '@/application/Common/Enum';
|
||||||
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Instructions/Data/InstructionsBuilder';
|
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
|
||||||
|
|
||||||
describe('InstructionListDataFactory', () => {
|
describe('InstructionListDataFactory', () => {
|
||||||
const supportedOsList = [OperatingSystem.macOS];
|
const supportedOsList = [OperatingSystem.macOS];
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { nextTick, watch } from 'vue';
|
||||||
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
|
import { useSelectedScriptNodeIds } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseSelectedScriptNodeIds';
|
||||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||||
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
import { UseAutoUnsubscribedEventsStub } from '@tests/unit/shared/Stubs/UseAutoUnsubscribedEventsStub';
|
||||||
@@ -8,9 +9,10 @@ import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCo
|
|||||||
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
import { SelectedScriptStub } from '@tests/unit/shared/Stubs/SelectedScriptStub';
|
||||||
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
import { getScriptNodeId } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/CategoryNodeMetadataConverter';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { UserSelectionStub } from '@tests/unit/shared/Stubs/UserSelectionStub';
|
||||||
|
|
||||||
describe('useSelectedScriptNodeIds', () => {
|
describe('useSelectedScriptNodeIds', () => {
|
||||||
it('returns empty array when no scripts are selected', () => {
|
it('returns an empty array when no scripts are selected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent();
|
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||||
useStateStub.withState(new CategoryCollectionStateStub().withSelectedScripts([]));
|
useStateStub.withState(new CategoryCollectionStateStub().withSelectedScripts([]));
|
||||||
@@ -19,38 +21,213 @@ describe('useSelectedScriptNodeIds', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(actualIds).to.have.lengthOf(0);
|
expect(actualIds).to.have.lengthOf(0);
|
||||||
});
|
});
|
||||||
|
it('initially registers the unsubscribe callback', () => {
|
||||||
it('returns correct node IDs for selected scripts', () => {
|
|
||||||
// arrange
|
// arrange
|
||||||
const selectedScripts = [
|
const eventsStub = new UseAutoUnsubscribedEventsStub();
|
||||||
new SelectedScriptStub('id-1'),
|
|
||||||
new SelectedScriptStub('id-2'),
|
|
||||||
];
|
|
||||||
const parsedNodeIds = new Map<IScript, string>([
|
|
||||||
[selectedScripts[0].script, 'expected-id-1'],
|
|
||||||
[selectedScripts[1].script, 'expected-id-1'],
|
|
||||||
]);
|
|
||||||
const { useStateStub, returnObject } = mountWrapperComponent({
|
|
||||||
scriptNodeIdParser: (script) => parsedNodeIds.get(script),
|
|
||||||
});
|
|
||||||
useStateStub.triggerOnStateChange({
|
|
||||||
newState: new CategoryCollectionStateStub().withSelectedScripts(selectedScripts),
|
|
||||||
immediateOnly: true,
|
|
||||||
});
|
|
||||||
// act
|
// act
|
||||||
const actualIds = returnObject.selectedScriptNodeIds.value;
|
mountWrapperComponent({
|
||||||
|
useAutoUnsubscribedEvents: eventsStub,
|
||||||
|
});
|
||||||
// assert
|
// assert
|
||||||
const expectedNodeIds = [...parsedNodeIds.values()];
|
const calls = eventsStub.events.callHistory;
|
||||||
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
expect(eventsStub.events.callHistory).has.lengthOf(1);
|
||||||
expect(actualIds).to.include.members(expectedNodeIds);
|
const call = calls.find((c) => c.methodName === 'unsubscribeAllAndRegister');
|
||||||
|
expect(call).toBeDefined();
|
||||||
|
});
|
||||||
|
describe('returns correct node IDs for selected scripts', () => {
|
||||||
|
it('immediately', () => {
|
||||||
|
// arrange
|
||||||
|
const selectedScripts = [
|
||||||
|
new SelectedScriptStub('id-1'),
|
||||||
|
new SelectedScriptStub('id-2'),
|
||||||
|
];
|
||||||
|
const parsedNodeIds = new Map<IScript, string>([
|
||||||
|
[selectedScripts[0].script, 'expected-id-1'],
|
||||||
|
[selectedScripts[1].script, 'expected-id-2'],
|
||||||
|
]);
|
||||||
|
const { useStateStub, returnObject } = mountWrapperComponent({
|
||||||
|
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
||||||
|
});
|
||||||
|
useStateStub.triggerOnStateChange({
|
||||||
|
newState: new CategoryCollectionStateStub().withSelectedScripts(selectedScripts),
|
||||||
|
immediateOnly: true,
|
||||||
|
});
|
||||||
|
// 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 collection state changes', () => {
|
||||||
|
// arrange
|
||||||
|
const initialScripts = [];
|
||||||
|
const changedScripts = [
|
||||||
|
new SelectedScriptStub('id-1'),
|
||||||
|
new SelectedScriptStub('id-2'),
|
||||||
|
];
|
||||||
|
const parsedNodeIds = new Map<IScript, string>([
|
||||||
|
[changedScripts[0].script, 'expected-id-1'],
|
||||||
|
[changedScripts[1].script, 'expected-id-2'],
|
||||||
|
]);
|
||||||
|
const { useStateStub, returnObject } = mountWrapperComponent({
|
||||||
|
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
||||||
|
});
|
||||||
|
useStateStub.triggerOnStateChange({
|
||||||
|
newState: new CategoryCollectionStateStub().withSelectedScripts(initialScripts),
|
||||||
|
immediateOnly: true,
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
useStateStub.triggerOnStateChange({
|
||||||
|
newState: new CategoryCollectionStateStub().withSelectedScripts(changedScripts),
|
||||||
|
immediateOnly: false,
|
||||||
|
});
|
||||||
|
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('id-1'),
|
||||||
|
new SelectedScriptStub('id-2'),
|
||||||
|
];
|
||||||
|
const parsedNodeIds = new Map<IScript, string>([
|
||||||
|
[changedScripts[0].script, 'expected-id-1'],
|
||||||
|
[changedScripts[1].script, 'expected-id-2'],
|
||||||
|
]);
|
||||||
|
const { useStateStub, returnObject } = mountWrapperComponent({
|
||||||
|
scriptNodeIdParser: createNodeIdParserFromMap(parsedNodeIds),
|
||||||
|
});
|
||||||
|
const userSelection = new UserSelectionStub([])
|
||||||
|
.withSelectedScripts(initialScripts);
|
||||||
|
useStateStub.triggerOnStateChange({
|
||||||
|
newState: new CategoryCollectionStateStub()
|
||||||
|
.withSelection(userSelection),
|
||||||
|
immediateOnly: true,
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
userSelection.triggerSelectionChangedEvent(changedScripts);
|
||||||
|
const actualIds = returnObject.selectedScriptNodeIds.value;
|
||||||
|
// assert
|
||||||
|
const expectedNodeIds = [...parsedNodeIds.values()];
|
||||||
|
expect(actualIds).to.have.lengthOf(expectedNodeIds.length);
|
||||||
|
expect(actualIds).to.include.members(expectedNodeIds);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('reactivity to state changes', () => {
|
||||||
|
describe('when the collection state changes', () => {
|
||||||
|
it('with new array references', async () => {
|
||||||
|
// arrange
|
||||||
|
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||||
|
let isChangeTriggered = false;
|
||||||
|
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
||||||
|
isChangeTriggered = true;
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
useStateStub.triggerOnStateChange({
|
||||||
|
newState: new CategoryCollectionStateStub(),
|
||||||
|
immediateOnly: false,
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
// assert
|
||||||
|
expect(isChangeTriggered).to.equal(true);
|
||||||
|
});
|
||||||
|
it('with the same array reference', async () => {
|
||||||
|
// arrange
|
||||||
|
const sharedSelectedScriptsReference = [];
|
||||||
|
const initialCollectionState = new CategoryCollectionStateStub()
|
||||||
|
.withSelectedScripts(sharedSelectedScriptsReference);
|
||||||
|
const changedCollectionState = new CategoryCollectionStateStub()
|
||||||
|
.withSelectedScripts(sharedSelectedScriptsReference);
|
||||||
|
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||||
|
useStateStub.triggerOnStateChange({
|
||||||
|
newState: initialCollectionState,
|
||||||
|
immediateOnly: true,
|
||||||
|
});
|
||||||
|
let isChangeTriggered = false;
|
||||||
|
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
||||||
|
isChangeTriggered = true;
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
sharedSelectedScriptsReference.push(new SelectedScriptStub('new')); // mutate array using same reference
|
||||||
|
useStateStub.triggerOnStateChange({
|
||||||
|
newState: changedCollectionState,
|
||||||
|
immediateOnly: false,
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
// assert
|
||||||
|
expect(isChangeTriggered).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when the selection state changes', () => {
|
||||||
|
it('with new array references', async () => {
|
||||||
|
// arrange
|
||||||
|
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||||
|
const userSelection = new UserSelectionStub([])
|
||||||
|
.withSelectedScripts([]);
|
||||||
|
useStateStub.triggerOnStateChange({
|
||||||
|
newState: new CategoryCollectionStateStub()
|
||||||
|
.withSelection(userSelection),
|
||||||
|
immediateOnly: true,
|
||||||
|
});
|
||||||
|
let isChangeTriggered = false;
|
||||||
|
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
||||||
|
isChangeTriggered = true;
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
userSelection.triggerSelectionChangedEvent([]);
|
||||||
|
await nextTick();
|
||||||
|
// assert
|
||||||
|
expect(isChangeTriggered).to.equal(true);
|
||||||
|
});
|
||||||
|
it('with the same array reference', async () => {
|
||||||
|
// arrange
|
||||||
|
const { useStateStub, returnObject } = mountWrapperComponent();
|
||||||
|
const sharedSelectedScriptsReference = [];
|
||||||
|
const userSelection = new UserSelectionStub([])
|
||||||
|
.withSelectedScripts(sharedSelectedScriptsReference);
|
||||||
|
useStateStub.triggerOnStateChange({
|
||||||
|
newState: new CategoryCollectionStateStub()
|
||||||
|
.withSelection(userSelection),
|
||||||
|
immediateOnly: true,
|
||||||
|
});
|
||||||
|
let isChangeTriggered = false;
|
||||||
|
watch(() => returnObject.selectedScriptNodeIds.value, () => {
|
||||||
|
isChangeTriggered = true;
|
||||||
|
});
|
||||||
|
// act
|
||||||
|
sharedSelectedScriptsReference.push(new SelectedScriptStub('new')); // mutate array using same reference
|
||||||
|
userSelection.triggerSelectionChangedEvent(sharedSelectedScriptsReference);
|
||||||
|
await nextTick();
|
||||||
|
// assert
|
||||||
|
expect(isChangeTriggered).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 mountWrapperComponent(scenario?: {
|
function mountWrapperComponent(scenario?: {
|
||||||
readonly scriptNodeIdParser?: typeof getScriptNodeId,
|
readonly scriptNodeIdParser?: ScriptNodeIdParser,
|
||||||
|
readonly useAutoUnsubscribedEvents?: UseAutoUnsubscribedEventsStub,
|
||||||
}) {
|
}) {
|
||||||
const useStateStub = new UseCollectionStateStub();
|
const useStateStub = new UseCollectionStateStub();
|
||||||
const nodeIdParser: typeof getScriptNodeId = scenario?.scriptNodeIdParser
|
const nodeIdParser: ScriptNodeIdParser = scenario?.scriptNodeIdParser
|
||||||
?? ((script) => script.id);
|
?? ((script) => script.id);
|
||||||
let returnObject: ReturnType<typeof useSelectedScriptNodeIds>;
|
let returnObject: ReturnType<typeof useSelectedScriptNodeIds>;
|
||||||
|
|
||||||
@@ -65,7 +242,7 @@ function mountWrapperComponent(scenario?: {
|
|||||||
[InjectionKeys.useCollectionState as symbol]:
|
[InjectionKeys.useCollectionState as symbol]:
|
||||||
() => useStateStub.get(),
|
() => useStateStub.get(),
|
||||||
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
[InjectionKeys.useAutoUnsubscribedEvents as symbol]:
|
||||||
() => new UseAutoUnsubscribedEventsStub().get(),
|
() => (scenario?.useAutoUnsubscribedEvents ?? new UseAutoUnsubscribedEventsStub()).get(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { BrowserClipboard, NavigatorClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard';
|
||||||
|
import { StubWithObservableMethodCalls } from '@tests/unit/shared/Stubs/StubWithObservableMethodCalls';
|
||||||
|
import { expectThrowsAsync } from '@tests/unit/shared/Assertions/ExpectThrowsAsync';
|
||||||
|
|
||||||
|
describe('BrowserClipboard', () => {
|
||||||
|
describe('writeText', () => {
|
||||||
|
it('calls navigator clipboard with the correct text', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedText = 'test text';
|
||||||
|
const navigatorClipboard = new NavigatorClipboardStub();
|
||||||
|
const clipboard = new BrowserClipboard(navigatorClipboard);
|
||||||
|
// act
|
||||||
|
await clipboard.copyText(expectedText);
|
||||||
|
// assert
|
||||||
|
const calls = navigatorClipboard.callHistory;
|
||||||
|
expect(calls).to.have.lengthOf(1);
|
||||||
|
const call = calls.find((c) => c.methodName === 'writeText');
|
||||||
|
expect(call).toBeDefined();
|
||||||
|
const [actualText] = call.args;
|
||||||
|
expect(actualText).to.equal(expectedText);
|
||||||
|
});
|
||||||
|
it('throws when navigator clipboard fails', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'internalError';
|
||||||
|
const navigatorClipboard = new NavigatorClipboardStub();
|
||||||
|
navigatorClipboard.writeText = () => {
|
||||||
|
throw new Error(expectedError);
|
||||||
|
};
|
||||||
|
const clipboard = new BrowserClipboard(navigatorClipboard);
|
||||||
|
// act
|
||||||
|
const act = () => clipboard.copyText('unimportant-text');
|
||||||
|
// assert
|
||||||
|
await expectThrowsAsync(act, expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class NavigatorClipboardStub
|
||||||
|
extends StubWithObservableMethodCalls<NavigatorClipboard>
|
||||||
|
implements NavigatorClipboard {
|
||||||
|
writeText(data: string): Promise<void> {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'writeText',
|
||||||
|
args: [data],
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
read(): Promise<ClipboardItems> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
readText(): Promise<string> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
write(): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchEvent(): boolean {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEventListener(): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { useClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/UseClipboard';
|
||||||
|
import { BrowserClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/BrowserClipboard';
|
||||||
|
import { ClipboardStub } from '@tests/unit/shared/Stubs/ClipboardStub';
|
||||||
|
import { FunctionKeys } from '@/TypeHelpers';
|
||||||
|
|
||||||
|
describe('useClipboard', () => {
|
||||||
|
it(`returns an instance of ${BrowserClipboard.name}`, () => {
|
||||||
|
// arrange
|
||||||
|
const expectedType = BrowserClipboard;
|
||||||
|
// act
|
||||||
|
const clipboard = useClipboard();
|
||||||
|
// assert
|
||||||
|
expect(clipboard).to.be.instanceOf(expectedType);
|
||||||
|
});
|
||||||
|
it('does not create a new instance if one is provided', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedClipboard = new ClipboardStub();
|
||||||
|
// act
|
||||||
|
const actualClipboard = useClipboard(expectedClipboard);
|
||||||
|
// assert
|
||||||
|
expect(actualClipboard).to.equal(expectedClipboard);
|
||||||
|
});
|
||||||
|
describe('supports object destructuring', () => {
|
||||||
|
type ClipboardFunction = FunctionKeys<ReturnType<typeof useClipboard>>;
|
||||||
|
const testScenarios: {
|
||||||
|
readonly [FunctionName in ClipboardFunction]:
|
||||||
|
Parameters<ReturnType<typeof useClipboard>[FunctionName]>;
|
||||||
|
} = {
|
||||||
|
copyText: ['text-arg'],
|
||||||
|
};
|
||||||
|
Object.entries(testScenarios).forEach(([functionName, testFunctionArgs]) => {
|
||||||
|
describe(functionName, () => {
|
||||||
|
it('binds the method to the instance', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedArgs = testFunctionArgs;
|
||||||
|
const clipboardStub = new ClipboardStub();
|
||||||
|
// act
|
||||||
|
const clipboard = useClipboard(clipboardStub);
|
||||||
|
const { [functionName as ClipboardFunction]: testFunction } = clipboard;
|
||||||
|
// assert
|
||||||
|
testFunction(...expectedArgs);
|
||||||
|
const call = clipboardStub.callHistory.find((c) => c.methodName === functionName);
|
||||||
|
expect(call).toBeDefined();
|
||||||
|
expect(call.args).to.deep.equal(expectedArgs);
|
||||||
|
});
|
||||||
|
it('ensures method retains the clipboard instance context', () => {
|
||||||
|
// arrange
|
||||||
|
const clipboardStub = new ClipboardStub();
|
||||||
|
const expectedThisContext = clipboardStub;
|
||||||
|
let actualThisContext: typeof expectedThisContext | undefined;
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
clipboardStub[functionName] = function () {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
actualThisContext = this;
|
||||||
|
};
|
||||||
|
// act
|
||||||
|
const clipboard = useClipboard(clipboardStub);
|
||||||
|
const { [functionName as ClipboardFunction]: testFunction } = clipboard;
|
||||||
|
// assert
|
||||||
|
testFunction(...testFunctionArgs);
|
||||||
|
expect(expectedThisContext).to.equal(actualThisContext);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
||||||
|
import { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode';
|
||||||
|
import { ApplicationCodeStub } from '@tests/unit/shared/Stubs/ApplicationCodeStub';
|
||||||
|
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
|
||||||
|
import { EventSubscriptionCollectionStub } from '@tests/unit/shared/Stubs/EventSubscriptionCollectionStub';
|
||||||
|
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
|
||||||
|
import { CodeChangedEventStub } from '@tests/unit/shared/Stubs/CodeChangedEventStub';
|
||||||
|
|
||||||
|
describe('useCurrentCode', () => {
|
||||||
|
describe('currentCode', () => {
|
||||||
|
it('gets code from initial state', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCode = 'initial code';
|
||||||
|
const { codeStub, collectionStateStub } = createStubs();
|
||||||
|
codeStub.withCurrentCode(expectedCode);
|
||||||
|
const useCollectionStateStub = new UseCollectionStateStub();
|
||||||
|
const { currentCode } = new UseCurrentCodeBuilder()
|
||||||
|
.withUseCollectionState(useCollectionStateStub)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
useCollectionStateStub.triggerOnStateChange({
|
||||||
|
newState: collectionStateStub,
|
||||||
|
immediateOnly: true, // set initial state
|
||||||
|
});
|
||||||
|
// assert
|
||||||
|
const actualCode = currentCode.value;
|
||||||
|
expect(actualCode).to.equal(expectedCode);
|
||||||
|
});
|
||||||
|
it('updates code state code is changed', () => {
|
||||||
|
// arrange
|
||||||
|
const initialCode = 'initial code';
|
||||||
|
const expectedCode = 'changed code';
|
||||||
|
const {
|
||||||
|
codeStub: initialCodeStub,
|
||||||
|
collectionStateStub: initialCollectionStateStub,
|
||||||
|
} = createStubs();
|
||||||
|
initialCodeStub.withCurrentCode(initialCode);
|
||||||
|
const useCollectionStateStub = new UseCollectionStateStub();
|
||||||
|
const { currentCode } = new UseCurrentCodeBuilder()
|
||||||
|
.withUseCollectionState(useCollectionStateStub)
|
||||||
|
.build();
|
||||||
|
useCollectionStateStub.triggerOnStateChange({
|
||||||
|
newState: initialCollectionStateStub,
|
||||||
|
immediateOnly: true, // set initial state
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
codeStub: changedCodeStub,
|
||||||
|
collectionStateStub: changedStateStub,
|
||||||
|
} = createStubs();
|
||||||
|
changedCodeStub.withCurrentCode(expectedCode);
|
||||||
|
// act
|
||||||
|
useCollectionStateStub.triggerOnStateChange({
|
||||||
|
newState: changedStateStub,
|
||||||
|
immediateOnly: true, // update state
|
||||||
|
});
|
||||||
|
// assert
|
||||||
|
const actualCode = currentCode.value;
|
||||||
|
expect(actualCode).to.equal(expectedCode);
|
||||||
|
});
|
||||||
|
it('updates code when code is changed', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCode = 'changed code';
|
||||||
|
const { codeStub, collectionStateStub } = createStubs();
|
||||||
|
const { currentCode } = new UseCurrentCodeBuilder()
|
||||||
|
.withCollectionState(collectionStateStub)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
codeStub.triggerCodeChange(new CodeChangedEventStub().withCode(expectedCode));
|
||||||
|
// assert
|
||||||
|
const actualCode = currentCode.value;
|
||||||
|
expect(expectedCode).to.equal(actualCode);
|
||||||
|
});
|
||||||
|
it('registers event subscription on creation', () => {
|
||||||
|
// arrange
|
||||||
|
const eventsStub = new EventSubscriptionCollectionStub();
|
||||||
|
const stateStub = new UseCollectionStateStub();
|
||||||
|
// act
|
||||||
|
new UseCurrentCodeBuilder()
|
||||||
|
.withUseCollectionState(stateStub)
|
||||||
|
.withEvents(eventsStub)
|
||||||
|
.build();
|
||||||
|
// assert
|
||||||
|
const calls = eventsStub.callHistory;
|
||||||
|
expect(calls).has.lengthOf(1);
|
||||||
|
const call = calls.find((c) => c.methodName === 'unsubscribeAllAndRegister');
|
||||||
|
expect(call).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createStubs() {
|
||||||
|
const codeStub = new ApplicationCodeStub();
|
||||||
|
const collectionStateStub = new CategoryCollectionStateStub().withCode(codeStub);
|
||||||
|
const useStateStub = new UseCollectionStateStub()
|
||||||
|
.withState(collectionStateStub);
|
||||||
|
return {
|
||||||
|
codeStub,
|
||||||
|
useStateStub,
|
||||||
|
collectionStateStub,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class UseCurrentCodeBuilder {
|
||||||
|
private useCollectionState: UseCollectionStateStub = new UseCollectionStateStub();
|
||||||
|
|
||||||
|
private events: IEventSubscriptionCollection = new EventSubscriptionCollectionStub();
|
||||||
|
|
||||||
|
public withUseCollectionState(useCollectionState: UseCollectionStateStub): this {
|
||||||
|
this.useCollectionState = useCollectionState;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withCollectionState(collectionState: CategoryCollectionStateStub): this {
|
||||||
|
return this.withUseCollectionState(
|
||||||
|
this.useCollectionState.withState(collectionState),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public withEvents(events: IEventSubscriptionCollection): this {
|
||||||
|
this.events = events;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): ReturnType<typeof useCurrentCode> {
|
||||||
|
return useCurrentCode(this.useCollectionState.get(), this.events);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,19 +55,31 @@ describe('AppIcon.vue', () => {
|
|||||||
`Expected:\n\n${expectedSvg}\n\nActual:\n\n${actualSvg}`,
|
`Expected:\n\n${expectedSvg}\n\nActual:\n\n${actualSvg}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('emits `click` event when clicked', async () => {
|
||||||
|
// arrange
|
||||||
|
const wrapper = mountComponent();
|
||||||
|
|
||||||
|
// act
|
||||||
|
await wrapper.trigger('click');
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(wrapper.emitted().click).to.have.lengthOf(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function mountComponent(options: {
|
function mountComponent(options?: {
|
||||||
readonly iconPropValue: IconName,
|
readonly iconPropValue?: IconName,
|
||||||
readonly loader: UseSvgLoaderStub,
|
readonly loader?: UseSvgLoaderStub,
|
||||||
}) {
|
}) {
|
||||||
|
const iconName = options?.iconPropValue ?? 'globe';
|
||||||
|
const loaderStub = options?.loader ?? new UseSvgLoaderStub().withSvgIcon(iconName, '<svg />');
|
||||||
return shallowMount(AppIcon, {
|
return shallowMount(AppIcon, {
|
||||||
props: {
|
props: {
|
||||||
icon: options.iconPropValue,
|
icon: iconName,
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
useSvgLoaderHook: options.loader.get(),
|
useSvgLoaderHook: loaderStub.get(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import { EventSourceStub } from './EventSourceStub';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
|
|
||||||
export class ApplicationCodeStub implements IApplicationCode {
|
export class ApplicationCodeStub implements IApplicationCode {
|
||||||
public changed: IEventSource<ICodeChangedEvent> = new EventSource<ICodeChangedEvent>();
|
public changed = new EventSourceStub<ICodeChangedEvent>();
|
||||||
|
|
||||||
public current = '';
|
public current = '';
|
||||||
|
|
||||||
|
public triggerCodeChange(event: ICodeChangedEvent): this {
|
||||||
|
this.changed.notify(event);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withCurrentCode(currentCode: string): this {
|
||||||
|
this.current = currentCode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { ApplicationCodeStub } from './ApplicationCodeStub';
|
|||||||
import { CategoryStub } from './CategoryStub';
|
import { CategoryStub } from './CategoryStub';
|
||||||
|
|
||||||
export class CategoryCollectionStateStub implements ICategoryCollectionState {
|
export class CategoryCollectionStateStub implements ICategoryCollectionState {
|
||||||
public readonly code: IApplicationCode = new ApplicationCodeStub();
|
public code: IApplicationCode = new ApplicationCodeStub();
|
||||||
|
|
||||||
public filter: IUserFilter = new UserFilterStub();
|
public filter: IUserFilter = new UserFilterStub();
|
||||||
|
|
||||||
@@ -39,6 +39,11 @@ export class CategoryCollectionStateStub implements ICategoryCollectionState {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public withCode(code: IApplicationCode): this {
|
||||||
|
this.code = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public withOs(os: OperatingSystem): this {
|
public withOs(os: OperatingSystem): this {
|
||||||
if (this.collection instanceof CategoryCollectionStub) {
|
if (this.collection instanceof CategoryCollectionStub) {
|
||||||
this.collection = this.collection.withOs(os);
|
this.collection = this.collection.withOs(os);
|
||||||
|
|||||||
14
tests/unit/shared/Stubs/ClipboardStub.ts
Normal file
14
tests/unit/shared/Stubs/ClipboardStub.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard';
|
||||||
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
|
|
||||||
|
export class ClipboardStub
|
||||||
|
extends StubWithObservableMethodCalls<Clipboard>
|
||||||
|
implements Clipboard {
|
||||||
|
public copyText(text: string): Promise<void> {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'copyText',
|
||||||
|
args: [text],
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
tests/unit/shared/Stubs/CodeChangedEventStub.ts
Normal file
26
tests/unit/shared/Stubs/CodeChangedEventStub.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||||
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
|
export class CodeChangedEventStub implements ICodeChangedEvent {
|
||||||
|
public code: string;
|
||||||
|
|
||||||
|
public addedScripts: readonly IScript[];
|
||||||
|
|
||||||
|
public removedScripts: readonly IScript[];
|
||||||
|
|
||||||
|
public changedScripts: readonly IScript[];
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getScriptPositionInCode(): ICodePosition {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public withCode(code: string): this {
|
||||||
|
this.code = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
tests/unit/shared/Stubs/UseClipboardStub.ts
Normal file
17
tests/unit/shared/Stubs/UseClipboardStub.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/UseClipboard';
|
||||||
|
import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard';
|
||||||
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
|
import { ClipboardStub } from './ClipboardStub';
|
||||||
|
|
||||||
|
export class UseClipboardStub
|
||||||
|
extends StubWithObservableMethodCalls<ReturnType<typeof useClipboard>> {
|
||||||
|
constructor(private readonly clipboard: Clipboard = new ClipboardStub()) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(): ReturnType<typeof useClipboard> {
|
||||||
|
const { clipboard } = this;
|
||||||
|
clipboard.copyText = clipboard.copyText.bind(clipboard);
|
||||||
|
return this.clipboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
tests/unit/shared/Stubs/UseCurrentCodeStub.ts
Normal file
17
tests/unit/shared/Stubs/UseCurrentCodeStub.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode';
|
||||||
|
|
||||||
|
export class UseCurrentCodeStub {
|
||||||
|
public currentCodeRef = ref('');
|
||||||
|
|
||||||
|
public withCurrentCode(code: string): this {
|
||||||
|
this.currentCodeRef.value = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(): ReturnType<typeof useCurrentCode> {
|
||||||
|
return {
|
||||||
|
currentCode: this.currentCodeRef,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
|
import { EventSourceStub } from './EventSourceStub';
|
||||||
|
|
||||||
export class UserSelectionStub
|
export class UserSelectionStub
|
||||||
extends StubWithObservableMethodCalls<IUserSelection>
|
extends StubWithObservableMethodCalls<IUserSelection>
|
||||||
implements IUserSelection {
|
implements IUserSelection {
|
||||||
public readonly changed: IEventSource<readonly SelectedScript[]> = new EventSource<
|
public readonly changed = new EventSourceStub<readonly SelectedScript[]>();
|
||||||
readonly SelectedScript[]>();
|
|
||||||
|
|
||||||
public selectedScripts: readonly SelectedScript[] = [];
|
public selectedScripts: readonly SelectedScript[] = [];
|
||||||
|
|
||||||
@@ -22,6 +20,11 @@ export class UserSelectionStub
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public triggerSelectionChangedEvent(scripts: readonly SelectedScript[]): this {
|
||||||
|
this.changed.notify(scripts);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public areAllSelected(): boolean {
|
public areAllSelected(): boolean {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user