Improve script error dialogs #304
- Include the script's directory path #304. - Exclude Windows-specific instructions on non-Windows OS. - Standardize language across dialogs for consistency. Other supporting changes: - Add script diagnostics data collection from main process. - Document script file storage and execution tamper protection in SECURITY.md. - Remove redundant comment in `NodeReadbackFileWriter`. - Centralize error display for uniformity and simplicity. - Simpify `WindowVariablesValidator` to omit checks when not on the renderer process. - Improve and centralize Electron environment detection. - Use more emphatic language (don't worry) in error messages.
This commit is contained in:
@@ -41,6 +41,10 @@ privacy.sexy adopts a defense in depth strategy to protect users on multiple lay
|
||||
The desktop application operates without persistent administrative or `sudo` privileges, reinforcing its security posture. It requests
|
||||
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
|
||||
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
|
||||
- **Secure Script Execution/Storage:**
|
||||
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans. This safeguards against
|
||||
any unwanted modifications. Furthermore, the application incorporates integrity checks for tamper protection. If the script file differs from
|
||||
the user's selected script, the application will not execute or save the script, ensuring the processing of authentic scripts.
|
||||
|
||||
### Update Security and Integrity
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ This table highlights differences between the desktop and web versions of `priva
|
||||
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
||||
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
|
||||
| [Error handling](#error-handling) | 🟢 Advanced | 🟡 Limited |
|
||||
| [Native dialogs](#error-handling) | 🟢 Available | 🔴 Not available |
|
||||
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
||||
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
||||
|
||||
## Feature descriptions
|
||||
|
||||
@@ -74,3 +75,21 @@ In contrast, the web version has more basic error handling due to browser limita
|
||||
|
||||
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
||||
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
||||
|
||||
### Secure script execution/storage
|
||||
|
||||
**Integrity checks:**
|
||||
|
||||
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
||||
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
||||
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
||||
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
||||
Due to browser constraints, this feature is absent in the web version.
|
||||
|
||||
**Error handling:**
|
||||
|
||||
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
||||
It also guides users through potential issues with filesystem or third-party software, such as antivirus interventions.
|
||||
Specifically, the application is capable of identifying when antivirus software blocks or removes a script, providing users with tailored error messages
|
||||
and detailed resolution steps. This level of proactive error handling and user guidance enhances the application's security and reliability,
|
||||
offering a feature not achievable in the web version due to browser limitations.
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface ScriptDiagnosticsCollector {
|
||||
collectDiagnosticInformation(): Promise<ScriptDiagnosticData>;
|
||||
}
|
||||
|
||||
export interface ScriptDiagnosticData {
|
||||
readonly scriptsDirectoryAbsolutePath?: string;
|
||||
readonly currentOperatingSystem?: OperatingSystem;
|
||||
}
|
||||
@@ -27,11 +27,6 @@ export class NodeReadbackFileWriter implements ReadbackFileWriter {
|
||||
const fileWritePipelineActions: ReadonlyArray<() => Promise<FileWriteOutcome>> = [
|
||||
() => this.createOrOverwriteFile(filePath, fileContents),
|
||||
() => this.verifyFileExistsWithoutReading(filePath),
|
||||
/*
|
||||
Reading the file contents back, we can detect if the file has been altered or
|
||||
removed post-creation. Removal of scripts when reading back is seen by some antivirus
|
||||
software when it falsely identifies a script as harmful.
|
||||
*/
|
||||
() => this.verifyFileContentsByReading(filePath, fileContents),
|
||||
];
|
||||
for (const action of fileWritePipelineActions) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ElectronEnvironmentDetector, ElectronProcessType } from './ElectronEnvironmentDetector';
|
||||
|
||||
export class ContextIsolatedElectronDetector implements ElectronEnvironmentDetector {
|
||||
constructor(
|
||||
private readonly nodeProcessAccessor: NodeProcessAccessor = () => globalThis?.process,
|
||||
private readonly userAgentAccessor: UserAgentAccessor = () => globalThis?.navigator?.userAgent,
|
||||
) { }
|
||||
|
||||
public isRunningInsideElectron(): boolean {
|
||||
return isNodeProcessElectronBased(this.nodeProcessAccessor)
|
||||
|| isUserAgentElectronBased(this.userAgentAccessor);
|
||||
}
|
||||
|
||||
public determineElectronProcessType(): ElectronProcessType {
|
||||
const isNodeAccessible = isNodeProcessElectronBased(this.nodeProcessAccessor);
|
||||
const isBrowserAccessible = isUserAgentElectronBased(this.userAgentAccessor);
|
||||
if (!isNodeAccessible && !isBrowserAccessible) {
|
||||
throw new Error('Unable to determine the Electron process type. Neither Node.js nor browser-based Electron contexts were detected.');
|
||||
}
|
||||
if (isNodeAccessible && isBrowserAccessible) {
|
||||
return 'preloader'; // Only preloader can access both Node.js and browser contexts in Electron with context isolation.
|
||||
}
|
||||
if (isNodeAccessible) {
|
||||
return 'main';
|
||||
}
|
||||
return 'renderer';
|
||||
}
|
||||
}
|
||||
|
||||
export type NodeProcessAccessor = () => NodeJS.Process | undefined;
|
||||
|
||||
function isNodeProcessElectronBased(nodeProcessAccessor: NodeProcessAccessor): boolean {
|
||||
const nodeProcess = nodeProcessAccessor();
|
||||
if (!nodeProcess) {
|
||||
return false;
|
||||
}
|
||||
if (nodeProcess.versions.electron) {
|
||||
// Electron populates `nodeProcess.versions.electron` with its version, see https://web.archive.org/web/20240113162837/https://www.electronjs.org/docs/latest/api/process#processversionselectron-readonly.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export type UserAgentAccessor = () => string | undefined;
|
||||
|
||||
function isUserAgentElectronBased(
|
||||
userAgentAccessor: UserAgentAccessor,
|
||||
): boolean {
|
||||
const userAgent = userAgentAccessor();
|
||||
if (userAgent?.includes('Electron')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ElectronEnvironmentDetector {
|
||||
isRunningInsideElectron(): boolean;
|
||||
determineElectronProcessType(): ElectronProcessType;
|
||||
}
|
||||
|
||||
export type ElectronProcessType = 'main' | 'preloader' | 'renderer';
|
||||
@@ -1,49 +1,32 @@
|
||||
import { ElectronEnvironmentDetector } from './Electron/ElectronEnvironmentDetector';
|
||||
import { BrowserRuntimeEnvironment } from './Browser/BrowserRuntimeEnvironment';
|
||||
import { NodeRuntimeEnvironment } from './Node/NodeRuntimeEnvironment';
|
||||
import { RuntimeEnvironment } from './RuntimeEnvironment';
|
||||
import { ContextIsolatedElectronDetector } from './Electron/ContextIsolatedElectronDetector';
|
||||
|
||||
export const CurrentEnvironment = determineAndCreateRuntimeEnvironment({
|
||||
window: globalThis.window,
|
||||
process: globalThis.process,
|
||||
});
|
||||
export const CurrentEnvironment = determineAndCreateRuntimeEnvironment(globalThis.window);
|
||||
|
||||
export function determineAndCreateRuntimeEnvironment(
|
||||
globalAccessor: GlobalPropertiesAccessor,
|
||||
globalWindow: Window | undefined | null = globalThis.window,
|
||||
electronDetector: ElectronEnvironmentDetector = new ContextIsolatedElectronDetector(),
|
||||
browserEnvironmentFactory: BrowserRuntimeEnvironmentFactory = (
|
||||
window,
|
||||
) => new BrowserRuntimeEnvironment(window),
|
||||
nodeEnvironmentFactory: NodeRuntimeEnvironmentFactory = (
|
||||
process: NodeJS.Process,
|
||||
) => new NodeRuntimeEnvironment(process),
|
||||
nodeEnvironmentFactory: NodeRuntimeEnvironmentFactory = () => new NodeRuntimeEnvironment(),
|
||||
): RuntimeEnvironment {
|
||||
if (isElectronMainProcess(globalAccessor.process)) {
|
||||
return nodeEnvironmentFactory(globalAccessor.process);
|
||||
if (
|
||||
electronDetector.isRunningInsideElectron()
|
||||
&& electronDetector.determineElectronProcessType() === 'main') {
|
||||
return nodeEnvironmentFactory();
|
||||
}
|
||||
const { window } = globalAccessor;
|
||||
if (!window) {
|
||||
if (!globalWindow) {
|
||||
throw new Error('Unsupported runtime environment: The current context is neither a recognized browser nor a desktop environment.');
|
||||
}
|
||||
return browserEnvironmentFactory(window);
|
||||
}
|
||||
|
||||
function isElectronMainProcess(
|
||||
nodeProcess: NodeJS.Process | undefined,
|
||||
): nodeProcess is NodeJS.Process {
|
||||
// Electron populates `nodeProcess.versions.electron` with its version, see https://web.archive.org/web/20240113162837/https://www.electronjs.org/docs/latest/api/process#processversionselectron-readonly.
|
||||
if (!nodeProcess) {
|
||||
return false;
|
||||
}
|
||||
if (nodeProcess.versions.electron) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return browserEnvironmentFactory(globalWindow);
|
||||
}
|
||||
|
||||
export type BrowserRuntimeEnvironmentFactory = (window: Window) => RuntimeEnvironment;
|
||||
|
||||
export type NodeRuntimeEnvironmentFactory = (process: NodeJS.Process) => NodeRuntimeEnvironment;
|
||||
export type NodeRuntimeEnvironmentFactory = () => NodeRuntimeEnvironment;
|
||||
|
||||
export interface GlobalPropertiesAccessor {
|
||||
readonly window: Window | undefined;
|
||||
readonly process: NodeJS.Process | undefined;
|
||||
}
|
||||
export type GlobalWindowAccessor = Window | undefined;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ScriptDiagnosticData, ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { PersistentDirectoryProvider } from '@/infrastructure/CodeRunner/Creation/Directory/PersistentDirectoryProvider';
|
||||
import { ScriptDirectoryProvider } from '../CodeRunner/Creation/Directory/ScriptDirectoryProvider';
|
||||
|
||||
export class ScriptEnvironmentDiagnosticsCollector implements ScriptDiagnosticsCollector {
|
||||
constructor(
|
||||
private readonly directoryProvider: ScriptDirectoryProvider = new PersistentDirectoryProvider(),
|
||||
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
|
||||
) { }
|
||||
|
||||
public async collectDiagnosticInformation(): Promise<ScriptDiagnosticData> {
|
||||
const { directoryAbsolutePath } = await this.directoryProvider.provideScriptDirectory();
|
||||
return {
|
||||
scriptsDirectoryAbsolutePath: directoryAbsolutePath,
|
||||
currentOperatingSystem: this.environment.os,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
|
||||
/* Primary entry point for platform-specific injections */
|
||||
export interface WindowVariables {
|
||||
@@ -10,4 +11,5 @@ export interface WindowVariables {
|
||||
readonly os?: OperatingSystem;
|
||||
readonly log?: Logger;
|
||||
readonly dialog?: Dialog;
|
||||
readonly scriptDiagnosticsCollector?: ScriptDiagnosticsCollector;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ContextIsolatedElectronDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ContextIsolatedElectronDetector';
|
||||
import { ElectronEnvironmentDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import {
|
||||
PropertyKeys, isBoolean, isFunction, isNumber, isPlainObject,
|
||||
@@ -7,7 +9,14 @@ import { WindowVariables } from './WindowVariables';
|
||||
/**
|
||||
* Checks for consistency in runtime environment properties injected by Electron preloader.
|
||||
*/
|
||||
export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
||||
export function validateWindowVariables(
|
||||
variables: Partial<WindowVariables>,
|
||||
electronDetector: ElectronEnvironmentDetector = new ContextIsolatedElectronDetector(),
|
||||
) {
|
||||
if (!electronDetector.isRunningInsideElectron()
|
||||
|| electronDetector.determineElectronProcessType() !== 'renderer') {
|
||||
return;
|
||||
}
|
||||
if (!isPlainObject(variables)) {
|
||||
throw new Error('window is not an object');
|
||||
}
|
||||
@@ -20,12 +29,11 @@ export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
||||
function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<string> {
|
||||
const tests: Record<PropertyKeys<Required<WindowVariables>>, boolean> = {
|
||||
os: testOperatingSystem(variables.os),
|
||||
isRunningAsDesktopApplication: testIsRunningAsDesktopApplication(
|
||||
variables.isRunningAsDesktopApplication,
|
||||
),
|
||||
isRunningAsDesktopApplication: testIsRunningAsDesktopApplication(variables),
|
||||
codeRunner: testCodeRunner(variables),
|
||||
log: testLogger(variables),
|
||||
dialog: testDialog(variables),
|
||||
scriptDiagnosticsCollector: testScriptDiagnosticsCollector(variables),
|
||||
};
|
||||
|
||||
for (const [propertyName, testResult] of Object.entries(tests)) {
|
||||
@@ -49,30 +57,30 @@ function testOperatingSystem(os: unknown): boolean {
|
||||
}
|
||||
|
||||
function testLogger(variables: Partial<WindowVariables>): boolean {
|
||||
if (!variables.isRunningAsDesktopApplication) {
|
||||
return true;
|
||||
}
|
||||
return isPlainObject(variables.log);
|
||||
return isPlainObject(variables.log)
|
||||
&& isFunction(variables.log.debug)
|
||||
&& isFunction(variables.log.info)
|
||||
&& isFunction(variables.log.error)
|
||||
&& isFunction(variables.log.warn);
|
||||
}
|
||||
|
||||
function testCodeRunner(variables: Partial<WindowVariables>): boolean {
|
||||
if (!variables.isRunningAsDesktopApplication) {
|
||||
return true;
|
||||
}
|
||||
return isPlainObject(variables.codeRunner)
|
||||
&& isFunction(variables.codeRunner.runCode);
|
||||
}
|
||||
|
||||
function testIsRunningAsDesktopApplication(isRunningAsDesktopApplication: unknown): boolean {
|
||||
if (isRunningAsDesktopApplication === undefined) {
|
||||
return true;
|
||||
}
|
||||
return isBoolean(isRunningAsDesktopApplication);
|
||||
function testIsRunningAsDesktopApplication(variables: Partial<WindowVariables>): boolean {
|
||||
return isBoolean(variables.isRunningAsDesktopApplication)
|
||||
&& variables.isRunningAsDesktopApplication === true;
|
||||
}
|
||||
|
||||
function testDialog(variables: Partial<WindowVariables>): boolean {
|
||||
if (!variables.isRunningAsDesktopApplication) {
|
||||
return true;
|
||||
}
|
||||
return isPlainObject(variables.dialog);
|
||||
return isPlainObject(variables.dialog)
|
||||
&& isFunction(variables.dialog.saveFile)
|
||||
&& isFunction(variables.dialog.showError);
|
||||
}
|
||||
|
||||
function testScriptDiagnosticsCollector(variables: Partial<WindowVariables>): boolean {
|
||||
return isPlainObject(variables.scriptDiagnosticsCollector)
|
||||
&& isFunction(variables.scriptDiagnosticsCollector.collectDiagnosticInformation);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useLogger } from '@/presentation/components/Shared/Hooks/Log/UseLogger'
|
||||
import { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRunner';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { useDialog } from '@/presentation/components/Shared/Hooks/Dialog/UseDialog';
|
||||
import { useScriptDiagnosticsCollector } from '@/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector';
|
||||
|
||||
export function provideDependencies(
|
||||
context: IApplicationContext,
|
||||
@@ -72,6 +73,10 @@ export function provideDependencies(
|
||||
InjectionKeys.useDialog,
|
||||
useDialog,
|
||||
),
|
||||
useScriptDiagnosticsCollector: (di) => di.provide(
|
||||
InjectionKeys.useScriptDiagnosticsCollector,
|
||||
useScriptDiagnosticsCollector,
|
||||
),
|
||||
};
|
||||
registerAll(Object.values(resolvers), api);
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { CodeRunError } from '@/application/CodeRunner/CodeRunner';
|
||||
import IconButton from './IconButton.vue';
|
||||
import { createScriptErrorDialog } from './ScriptErrorDialog';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -24,6 +23,7 @@ export default defineComponent({
|
||||
const { os, isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { codeRunner } = injectKey((keys) => keys.useCodeRunner);
|
||||
const { dialog } = injectKey((keys) => keys.useDialog);
|
||||
const { scriptDiagnosticsCollector } = injectKey((keys) => keys.useScriptDiagnosticsCollector);
|
||||
|
||||
const canRun = computed<boolean>(() => getCanRunState(
|
||||
currentState.value.os,
|
||||
@@ -38,7 +38,12 @@ export default defineComponent({
|
||||
currentContext.state.collection.scripting.fileExtension,
|
||||
);
|
||||
if (!success) {
|
||||
showScriptRunError(dialog, error);
|
||||
dialog.showError(...(await createScriptErrorDialog({
|
||||
errorContext: 'run',
|
||||
errorType: error.type,
|
||||
errorMessage: error.message,
|
||||
isFileReadbackError: error.type === 'FileReadbackVerificationError',
|
||||
}, scriptDiagnosticsCollector)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,65 +62,4 @@ function getCanRunState(
|
||||
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||
return isRunningAsDesktopApplication && isRunningOnSelectedOs;
|
||||
}
|
||||
|
||||
function showScriptRunError(dialog: Dialog, error: CodeRunError) {
|
||||
const technicalDetails = `[${error.type}] ${error.message}`;
|
||||
dialog.showError(
|
||||
...(
|
||||
error.type === 'FileReadbackVerificationError'
|
||||
? createAntivirusErrorDialog(technicalDetails)
|
||||
: createGenericErrorDialog(technicalDetails)),
|
||||
);
|
||||
}
|
||||
|
||||
function createGenericErrorDialog(technicalDetails: string): Parameters<Dialog['showError']> {
|
||||
return [
|
||||
'Error Running Script',
|
||||
[
|
||||
'We encountered an issue while running the script.',
|
||||
'This could be due to a variety of factors such as system permissions, resource constraints, or security software interventions.',
|
||||
'\n',
|
||||
'Here are some steps you can take:',
|
||||
'- Confirm that you have the necessary permissions to execute scripts on your system.',
|
||||
'- Check if there is sufficient disk space and system resources available.',
|
||||
[
|
||||
'- Antivirus or security software can sometimes mistakenly block script execution.',
|
||||
'Verify your security settings, or temporarily disable the security software to see if that resolves the issue.',
|
||||
'privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software.',
|
||||
].join(' '),
|
||||
'- If possible, try running a different script to determine if the issue is specific to a particular script.',
|
||||
'- Should the problem persist, reach out to the community for further assistance.',
|
||||
'\n',
|
||||
'Technical Details:',
|
||||
technicalDetails,
|
||||
].join('\n'),
|
||||
];
|
||||
}
|
||||
|
||||
function createAntivirusErrorDialog(technicalDetails: string): Parameters<Dialog['showError']> {
|
||||
return [
|
||||
'Potential Antivirus Intervention',
|
||||
[
|
||||
[
|
||||
'We\'ve encountered a problem which may be due to your antivirus software intervening.',
|
||||
'privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software such as Defender.',
|
||||
].join(' '),
|
||||
'\n',
|
||||
'To address this, you can:',
|
||||
'1. Temporarily disable your antivirus (real-time protection) or add an exclusion for privacy.sexy scripts.',
|
||||
'2. Re-try running or downloading the script.',
|
||||
'3. If the issue persists, check your antivirus logs for more details and consider reporting this as a false positive to your antivirus provider.',
|
||||
'\n',
|
||||
'To handle false warnings in Defender: Open "Virus & threat protection" from the "Start" menu.',
|
||||
'\n',
|
||||
[
|
||||
'Remember to re-enable your antivirus protection as soon as possible for your security.',
|
||||
'For more guidance, refer to your antivirus documentation.',
|
||||
].join(' '),
|
||||
'\n',
|
||||
'Technical Details:',
|
||||
technicalDetails,
|
||||
].join('\n'),
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,8 +20,9 @@ import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue'
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptFilename } from '@/application/CodeRunner/ScriptFilename';
|
||||
import { Dialog, FileType, SaveFileError } from '@/presentation/common/Dialog';
|
||||
import { FileType } from '@/presentation/common/Dialog';
|
||||
import IconButton from '../IconButton.vue';
|
||||
import { createScriptErrorDialog } from '../ScriptErrorDialog';
|
||||
import RunInstructions from './RunInstructions/RunInstructions.vue';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -34,6 +35,7 @@ export default defineComponent({
|
||||
const { currentState } = injectKey((keys) => keys.useCollectionState);
|
||||
const { isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { dialog } = injectKey((keys) => keys.useDialog);
|
||||
const { scriptDiagnosticsCollector } = injectKey((keys) => keys.useScriptDiagnosticsCollector);
|
||||
|
||||
const areInstructionsVisible = ref(false);
|
||||
const filename = computed<string>(() => buildFilename(currentState.value.collection.scripting));
|
||||
@@ -45,7 +47,12 @@ export default defineComponent({
|
||||
getType(currentState.value.collection.scripting.language),
|
||||
);
|
||||
if (!success) {
|
||||
showScriptSaveError(dialog, error);
|
||||
dialog.showError(...(await createScriptErrorDialog({
|
||||
errorContext: 'save',
|
||||
errorType: error.type,
|
||||
errorMessage: error.message,
|
||||
isFileReadbackError: error.type === 'FileReadbackVerificationError',
|
||||
}, scriptDiagnosticsCollector)));
|
||||
return;
|
||||
}
|
||||
areInstructionsVisible.value = true;
|
||||
@@ -77,60 +84,4 @@ function buildFilename(scripting: IScriptingDefinition) {
|
||||
}
|
||||
return ScriptFilename;
|
||||
}
|
||||
|
||||
function showScriptSaveError(dialog: Dialog, error: SaveFileError) {
|
||||
const technicalDetails = `[${error.type}] ${error.message}`;
|
||||
dialog.showError(
|
||||
...(
|
||||
error.type === 'FileReadbackVerificationError'
|
||||
? createAntivirusErrorDialog(technicalDetails)
|
||||
: createGenericErrorDialog(technicalDetails)),
|
||||
);
|
||||
}
|
||||
|
||||
function createGenericErrorDialog(technicalDetails: string): Parameters<Dialog['showError']> {
|
||||
return [
|
||||
'Error Saving Script',
|
||||
[
|
||||
'An error occurred while saving the script.',
|
||||
'This issue may arise from insufficient permissions, limited disk space, or interference from security software.',
|
||||
'\n',
|
||||
'To address this:',
|
||||
'- Verify your permissions for the selected save directory.',
|
||||
'- Check available disk space.',
|
||||
'- Review your antivirus or security settings; adding an exclusion for privacy.sexy might be necessary.',
|
||||
'- Try saving the script to a different location or modifying your selection.',
|
||||
'- If the problem persists, reach out to the community for further assistance.',
|
||||
'\n',
|
||||
'Technical Details:',
|
||||
technicalDetails,
|
||||
].join('\n'),
|
||||
];
|
||||
}
|
||||
|
||||
function createAntivirusErrorDialog(technicalDetails: string): Parameters<Dialog['showError']> {
|
||||
return [
|
||||
'Potential Antivirus Intervention',
|
||||
[
|
||||
[
|
||||
'It seems your antivirus software might have blocked the saving of the script.',
|
||||
'privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software such as Defender.',
|
||||
].join(' '),
|
||||
'\n',
|
||||
'To resolve this, consider:',
|
||||
'1. Checking your antivirus for any blocking notifications and allowing the script.',
|
||||
'2. Temporarily disabling real-time protection or adding an exclusion for privacy.sexy scripts.',
|
||||
'3. Re-attempting to save the script.',
|
||||
'4. If the problem continues, review your antivirus logs for more details.',
|
||||
'\n',
|
||||
'To handle false warnings in Defender: Open "Virus & threat protection" from the "Start" menu.',
|
||||
'\n',
|
||||
'Always ensure to re-enable your antivirus protection promptly.',
|
||||
'For more guidance, refer to your antivirus documentation.',
|
||||
'\n',
|
||||
'Technical Details:',
|
||||
technicalDetails,
|
||||
].join('\n'),
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
</p>
|
||||
<p>
|
||||
These false positives are common for scripts that modify system settings.
|
||||
privacy.sexy is secure, transparent, and open-source.
|
||||
Don't worry; privacy.sexy is secure, transparent, and open-source.
|
||||
</p>
|
||||
<p>
|
||||
To handle false warnings in Microsoft Defender:
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { ScriptDiagnosticData, ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
|
||||
export async function createScriptErrorDialog(
|
||||
information: ScriptErrorDetails,
|
||||
scriptDiagnosticsCollector: ScriptDiagnosticsCollector | undefined,
|
||||
): Promise<Parameters<Dialog['showError']>> {
|
||||
const diagnostics = await scriptDiagnosticsCollector?.collectDiagnosticInformation();
|
||||
if (information.isFileReadbackError) {
|
||||
return createAntivirusErrorDialog(information, diagnostics);
|
||||
}
|
||||
return createGenericErrorDialog(information, diagnostics);
|
||||
}
|
||||
|
||||
export interface ScriptErrorDetails {
|
||||
readonly errorContext: 'run' | 'save';
|
||||
readonly errorType: string;
|
||||
readonly errorMessage: string;
|
||||
readonly isFileReadbackError: boolean;
|
||||
}
|
||||
|
||||
function createGenericErrorDialog(
|
||||
information: ScriptErrorDetails,
|
||||
diagnostics: ScriptDiagnosticData | undefined,
|
||||
): Parameters<Dialog['showError']> {
|
||||
return [
|
||||
selectBasedOnErrorContext({
|
||||
runningScript: 'Error Running Script',
|
||||
savingScript: 'Error Saving Script',
|
||||
}, information),
|
||||
[
|
||||
selectBasedOnErrorContext({
|
||||
runningScript: 'An error occurred while running the script.',
|
||||
savingScript: 'An error occurred while saving the script.',
|
||||
}, information),
|
||||
'This error could be caused by insufficient permissions, limited disk space, or security software interference.',
|
||||
'\n',
|
||||
generateUnorderedSolutionList({
|
||||
title: 'To address this, you can:',
|
||||
solutions: [
|
||||
'Check if there is enough disk space and system resources are available.',
|
||||
selectBasedOnDirectoryPath({
|
||||
withoutDirectoryPath: 'Verify your access rights to the script\'s folder.',
|
||||
withDirectoryPath: (directory) => `Verify your access rights to the script's folder: "${directory}".`,
|
||||
}, diagnostics),
|
||||
[
|
||||
'Check if antivirus or security software has mistakenly blocked the script.',
|
||||
'Don\'t worry; privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software.',
|
||||
'Temporarily disabling the security software may resolve this.',
|
||||
].join(' '),
|
||||
selectBasedOnErrorContext({
|
||||
runningScript: 'Confirm that you have the necessary permissions to execute scripts on your system.',
|
||||
savingScript: 'Try saving the script to a different location.',
|
||||
}, information),
|
||||
generateTryDifferentSelectionAdvice(information),
|
||||
'If the problem persists, reach out to the community for further assistance.',
|
||||
],
|
||||
}),
|
||||
'\n',
|
||||
generateTechnicalDetails(information),
|
||||
].join('\n'),
|
||||
];
|
||||
}
|
||||
|
||||
function createAntivirusErrorDialog(
|
||||
information: ScriptErrorDetails,
|
||||
diagnostics: ScriptDiagnosticData | undefined,
|
||||
): Parameters<Dialog['showError']> {
|
||||
const defenderSteps = generateDefenderSteps(information, diagnostics);
|
||||
return [
|
||||
'Possible Antivirus Script Block',
|
||||
[
|
||||
[
|
||||
'It seems your antivirus software might have removed the script.',
|
||||
'Don\'t worry; privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software.',
|
||||
].join(' '),
|
||||
'\n',
|
||||
selectBasedOnErrorContext({
|
||||
savingScript: generateOrderedSolutionList({
|
||||
title: 'To address this, you can:',
|
||||
solutions: [
|
||||
'Check your antivirus for any blocking notifications and allow the script.',
|
||||
'Disable antivirus or security software temporarily or add an exclusion.',
|
||||
'Save the script again.',
|
||||
],
|
||||
}),
|
||||
runningScript: generateOrderedSolutionList({
|
||||
title: 'To address this, you can:',
|
||||
solutions: [
|
||||
selectBasedOnDirectoryPath({
|
||||
withoutDirectoryPath: 'Disable antivirus or security software temporarily or add an exclusion.',
|
||||
withDirectoryPath: (directory) => `Disable antivirus or security software temporarily or add a directory exclusion for scripts executed from: "${directory}".`,
|
||||
}, diagnostics),
|
||||
'Run the script again.',
|
||||
],
|
||||
}),
|
||||
}, information),
|
||||
defenderSteps ? `\n${defenderSteps}\n` : '\n',
|
||||
[
|
||||
'It\'s important to re-enable your antivirus protection after resolving the issue for your security.',
|
||||
'For more guidance, refer to your antivirus documentation.',
|
||||
].join(' '),
|
||||
'\n',
|
||||
generateUnorderedSolutionList({
|
||||
title: 'If the problem persists:',
|
||||
solutions: [
|
||||
generateTryDifferentSelectionAdvice(information),
|
||||
'Consider reporting this as a false positive to your antivirus provider.',
|
||||
'Review your antivirus logs for more details.',
|
||||
'Reach out to the community for further assistance.',
|
||||
],
|
||||
}),
|
||||
'\n',
|
||||
generateTechnicalDetails(information),
|
||||
].join('\n'),
|
||||
];
|
||||
}
|
||||
|
||||
interface SolutionListOptions {
|
||||
readonly solutions: readonly string[];
|
||||
readonly title: string;
|
||||
}
|
||||
|
||||
function generateUnorderedSolutionList(options: SolutionListOptions) {
|
||||
return [
|
||||
options.title,
|
||||
...options.solutions.map((step) => `- ${step}`),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function generateTechnicalDetails(information: ScriptErrorDetails) {
|
||||
const maxErrorMessageCharacters = 100;
|
||||
const trimmedErrorMessage = information.errorMessage.length > maxErrorMessageCharacters
|
||||
? `${information.errorMessage.substring(0, maxErrorMessageCharacters - 3)}...`
|
||||
: information.errorMessage;
|
||||
return `Technical Details: [${information.errorType}] ${trimmedErrorMessage}`;
|
||||
}
|
||||
|
||||
function generateTryDifferentSelectionAdvice(information: ScriptErrorDetails) {
|
||||
return selectBasedOnErrorContext({
|
||||
runningScript: 'Run a different script selection to check if the problem is script-specific.',
|
||||
savingScript: 'Save a different script selection to check if the problem is script-specific.',
|
||||
}, information);
|
||||
}
|
||||
|
||||
function selectBasedOnDirectoryPath<T>(
|
||||
options: {
|
||||
readonly withoutDirectoryPath: T,
|
||||
withDirectoryPath: (directoryPath: string) => T,
|
||||
},
|
||||
diagnostics: ScriptDiagnosticData | undefined,
|
||||
): T {
|
||||
if (!diagnostics?.scriptsDirectoryAbsolutePath) {
|
||||
return options.withoutDirectoryPath;
|
||||
}
|
||||
return options.withDirectoryPath(diagnostics.scriptsDirectoryAbsolutePath);
|
||||
}
|
||||
|
||||
function generateOrderedSolutionList(options: SolutionListOptions): string {
|
||||
return [
|
||||
options.title,
|
||||
...options.solutions.map((step, index) => `${index + 1}. ${step}`),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function generateDefenderSteps(
|
||||
information: ScriptErrorDetails,
|
||||
diagnostics: ScriptDiagnosticData | undefined,
|
||||
): string | undefined {
|
||||
if (diagnostics?.currentOperatingSystem !== OperatingSystem.Windows) {
|
||||
return undefined;
|
||||
}
|
||||
return generateOrderedSolutionList({
|
||||
title: 'To handle false warnings in Defender:',
|
||||
solutions: [
|
||||
'Open "Virus & threat protection" via the "Start" menu.',
|
||||
'Open "Manage settings" under "Virus & threat protection settings" heading.',
|
||||
...selectBasedOnErrorContext({
|
||||
savingScript: [
|
||||
'Disable "Real-time protection" or add an exclusion by selecting "Add or remove exclusions".',
|
||||
],
|
||||
runningScript: selectBasedOnDirectoryPath({
|
||||
withoutDirectoryPath: [
|
||||
'Disable real-time protection or add exclusion for scripts.',
|
||||
],
|
||||
withDirectoryPath: (directory) => [
|
||||
'Open "Add or remove exclusions" under "Add or remove exclusions".',
|
||||
`Add directory exclusion for "${directory}".`,
|
||||
],
|
||||
}, diagnostics),
|
||||
}, information),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function selectBasedOnErrorContext<T>(options: {
|
||||
readonly savingScript: T;
|
||||
readonly runningScript: T;
|
||||
}, information: ScriptErrorDetails): T {
|
||||
if (information.errorContext === 'run') {
|
||||
return options.runningScript;
|
||||
}
|
||||
return options.savingScript;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
|
||||
export function useScriptDiagnosticsCollector(
|
||||
window: Partial<WindowVariables> = globalThis.window,
|
||||
) {
|
||||
return {
|
||||
scriptDiagnosticsCollector: window?.scriptDiagnosticsCollector,
|
||||
};
|
||||
}
|
||||
@@ -3,17 +3,22 @@ import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { ElectronDialog } from '@/infrastructure/Dialog/Electron/ElectronDialog';
|
||||
import { IpcChannel } from '@/presentation/electron/shared/IpcBridging/IpcChannel';
|
||||
import { ScriptEnvironmentDiagnosticsCollector } from '@/infrastructure/ScriptDiagnostics/ScriptEnvironmentDiagnosticsCollector';
|
||||
import { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
import { registerIpcChannel } from '../shared/IpcBridging/IpcProxy';
|
||||
import { ChannelDefinitionKey, IpcChannelDefinitions } from '../shared/IpcBridging/IpcChannelDefinitions';
|
||||
|
||||
export function registerAllIpcChannels(
|
||||
registrar: IpcChannelRegistrar = registerIpcChannel,
|
||||
createCodeRunner: CodeRunnerFactory = () => new ScriptFileCodeRunner(),
|
||||
createDialog: DialogFactory = () => new ElectronDialog(),
|
||||
registrar: IpcChannelRegistrar = registerIpcChannel,
|
||||
createScriptDiagnosticsCollector
|
||||
: ScriptDiagnosticsCollectorFactory = () => new ScriptEnvironmentDiagnosticsCollector(),
|
||||
) {
|
||||
const ipcInstanceCreators: IpcChannelRegistrars = {
|
||||
CodeRunner: () => createCodeRunner(),
|
||||
Dialog: () => createDialog(),
|
||||
ScriptDiagnosticsCollector: () => createScriptDiagnosticsCollector(),
|
||||
};
|
||||
Object.entries(ipcInstanceCreators).forEach(([name, instanceFactory]) => {
|
||||
try {
|
||||
@@ -26,9 +31,11 @@ export function registerAllIpcChannels(
|
||||
});
|
||||
}
|
||||
|
||||
export type IpcChannelRegistrar = typeof registerIpcChannel;
|
||||
|
||||
export type CodeRunnerFactory = () => CodeRunner;
|
||||
export type DialogFactory = () => Dialog;
|
||||
export type IpcChannelRegistrar = typeof registerIpcChannel;
|
||||
export type ScriptDiagnosticsCollectorFactory = () => ScriptDiagnosticsCollector;
|
||||
|
||||
type RegistrationChannel<T extends ChannelDefinitionKey> = (typeof IpcChannelDefinitions)[T];
|
||||
type ExtractChannelServiceType<T> = T extends IpcChannel<infer U> ? U : never;
|
||||
|
||||
@@ -7,10 +7,10 @@ import { IpcChannelDefinitions } from '../../shared/IpcBridging/IpcChannelDefini
|
||||
import { createSecureFacade } from './SecureFacadeCreator';
|
||||
|
||||
export function provideWindowVariables(
|
||||
createLogger: LoggerFactory = () => createElectronLogger(),
|
||||
convertToOs = convertPlatformToOs,
|
||||
createApiFacade: ApiFacadeFactory = createSecureFacade,
|
||||
ipcConsumerCreator: IpcConsumerProxyCreator = createIpcConsumerProxy,
|
||||
convertToOs = convertPlatformToOs,
|
||||
createLogger: LoggerFactory = () => createElectronLogger(),
|
||||
): WindowVariables {
|
||||
// Enforces mandatory variable availability at compile time
|
||||
const variables: RequiredWindowVariables = {
|
||||
@@ -19,6 +19,9 @@ export function provideWindowVariables(
|
||||
os: convertToOs(process.platform),
|
||||
codeRunner: ipcConsumerCreator(IpcChannelDefinitions.CodeRunner),
|
||||
dialog: ipcConsumerCreator(IpcChannelDefinitions.Dialog),
|
||||
scriptDiagnosticsCollector: ipcConsumerCreator(
|
||||
IpcChannelDefinitions.ScriptDiagnosticsCollector,
|
||||
),
|
||||
};
|
||||
return variables;
|
||||
}
|
||||
@@ -26,8 +29,8 @@ export function provideWindowVariables(
|
||||
type RequiredWindowVariables = PartiallyRequired<WindowVariables, 'os' /* | 'anotherOptionalKey'.. */>;
|
||||
type PartiallyRequired<T, K extends keyof T> = Required<Omit<T, K>> & Pick<T, K>;
|
||||
|
||||
export type LoggerFactory = () => Logger;
|
||||
|
||||
export type ApiFacadeFactory = typeof createSecureFacade;
|
||||
|
||||
export type IpcConsumerProxyCreator = typeof createIpcConsumerProxy;
|
||||
|
||||
export type LoggerFactory = () => Logger;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { FunctionKeys } from '@/TypeHelpers';
|
||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
import { IpcChannel } from './IpcChannel';
|
||||
|
||||
export const IpcChannelDefinitions = {
|
||||
CodeRunner: defineElectronIpcChannel<CodeRunner>('code-run', ['runCode']),
|
||||
Dialog: defineElectronIpcChannel<Dialog>('dialogs', ['showError', 'saveFile']),
|
||||
ScriptDiagnosticsCollector: defineElectronIpcChannel<ScriptDiagnosticsCollector>('script-diagnostics-collector', ['collectDiagnosticInformation']),
|
||||
} as const;
|
||||
|
||||
export type ChannelDefinitionKey = keyof typeof IpcChannelDefinitions;
|
||||
|
||||
@@ -51,8 +51,11 @@
|
||||
</style>
|
||||
<div id="javascriptDisabled">
|
||||
<h1>Problem loading page</h1>
|
||||
<p>The page does not work without JavaScript enabled. Please enable it to use privacy.sexy. There's no shady stuff
|
||||
as 100% of the website is open source.</p>
|
||||
<p>
|
||||
The page does not work without JavaScript enabled.
|
||||
Please enable it to use privacy.sexy.
|
||||
Don't worry; privacy.sexy is secure, transparent, and open-source.
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { useUserSelectionState } from '@/presentation/components/Shared/Hoo
|
||||
import type { useLogger } from '@/presentation/components/Shared/Hooks/Log/UseLogger';
|
||||
import type { useCodeRunner } from './components/Shared/Hooks/UseCodeRunner';
|
||||
import type { useDialog } from './components/Shared/Hooks/Dialog/UseDialog';
|
||||
import type { useScriptDiagnosticsCollector } from './components/Shared/Hooks/UseScriptDiagnosticsCollector';
|
||||
|
||||
export const InjectionKeys = {
|
||||
useCollectionState: defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState'),
|
||||
@@ -21,6 +22,7 @@ export const InjectionKeys = {
|
||||
useLogger: defineTransientKey<ReturnType<typeof useLogger>>('useLogger'),
|
||||
useCodeRunner: defineTransientKey<ReturnType<typeof useCodeRunner>>('useCodeRunner'),
|
||||
useDialog: defineTransientKey<ReturnType<typeof useDialog>>('useDialog'),
|
||||
useScriptDiagnosticsCollector: defineTransientKey<ReturnType<typeof useScriptDiagnosticsCollector>>('useScriptDiagnostics'),
|
||||
};
|
||||
|
||||
export interface InjectionKeyWithLifetime<T> {
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ContextIsolatedElectronDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ContextIsolatedElectronDetector';
|
||||
import { ElectronProcessType } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
|
||||
|
||||
describe('ContextIsolatedElectronDetector', () => {
|
||||
describe('isRunningInsideElectron', () => {
|
||||
describe('detects Electron environment correctly', () => {
|
||||
it('returns true on Electron main process', () => {
|
||||
// arrange
|
||||
const expectedValue = true;
|
||||
const process = createProcessStub({ isElectron: true });
|
||||
const userAgent = undefined;
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.isRunningInsideElectron();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedValue);
|
||||
});
|
||||
it('returns true on Electron preloader process', () => {
|
||||
// arrange
|
||||
const expectedValue = true;
|
||||
const process = createProcessStub({ isElectron: true });
|
||||
const userAgent = getElectronUserAgent();
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.isRunningInsideElectron();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedValue);
|
||||
});
|
||||
it('returns true on Electron renderer process', () => {
|
||||
// arrange
|
||||
const expectedValue = true;
|
||||
const process = undefined;
|
||||
const userAgent = getElectronUserAgent();
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.isRunningInsideElectron();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedValue);
|
||||
});
|
||||
it('returns false on non-Electron environment', () => {
|
||||
// arrange
|
||||
const expectedValue = false;
|
||||
const process = undefined;
|
||||
const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'; // non-Electron
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.isRunningInsideElectron();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedValue);
|
||||
});
|
||||
});
|
||||
describe('determineElectronProcessType', () => {
|
||||
it('gets Electron process type as main', () => {
|
||||
// arrange
|
||||
const expectedProcessType: ElectronProcessType = 'main';
|
||||
const process = createProcessStub({ isElectron: true });
|
||||
const userAgent = undefined;
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.determineElectronProcessType();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedProcessType);
|
||||
});
|
||||
it('gets Electron process type as preloader', () => {
|
||||
// arrange
|
||||
const expectedProcessType: ElectronProcessType = 'preloader';
|
||||
const process = createProcessStub({ isElectron: true });
|
||||
const userAgent = getElectronUserAgent();
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.determineElectronProcessType();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedProcessType);
|
||||
});
|
||||
it('gets Electron process type as renderer', () => {
|
||||
// arrange
|
||||
const expectedProcessType: ElectronProcessType = 'renderer';
|
||||
const process = undefined;
|
||||
const userAgent = getElectronUserAgent();
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const actualValue = detector.determineElectronProcessType();
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedProcessType);
|
||||
});
|
||||
it('throws non-Electron environment', () => {
|
||||
// arrange
|
||||
const expectedError = 'Unable to determine the Electron process type. Neither Node.js nor browser-based Electron contexts were detected.';
|
||||
const process = undefined;
|
||||
const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'; // non-Electron
|
||||
const detector = new ContextIsolatedElectronDetectorBuilder()
|
||||
.withProcess(process)
|
||||
.withUserAgent(userAgent)
|
||||
.build();
|
||||
// act
|
||||
const act = () => detector.determineElectronProcessType();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class ContextIsolatedElectronDetectorBuilder {
|
||||
private process: NodeJS.Process | undefined;
|
||||
|
||||
private userAgent: string | undefined;
|
||||
|
||||
public withProcess(process: NodeJS.Process | undefined): this {
|
||||
this.process = process;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withUserAgent(userAgent: string | undefined): this {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): ContextIsolatedElectronDetector {
|
||||
return new ContextIsolatedElectronDetector(
|
||||
() => this.process,
|
||||
() => this.userAgent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getElectronUserAgent() {
|
||||
return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.54 Electron/27.0.0 Safari/537.36';
|
||||
}
|
||||
|
||||
function createProcessStub(options?: {
|
||||
readonly isElectron: boolean;
|
||||
}): NodeJS.Process {
|
||||
if (options?.isElectron === true) {
|
||||
return {
|
||||
versions: {
|
||||
electron: '28.1.3',
|
||||
} as NodeJS.ProcessVersions,
|
||||
} as NodeJS.Process;
|
||||
}
|
||||
return {} as NodeJS.Process;
|
||||
}
|
||||
@@ -1,58 +1,38 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
BrowserRuntimeEnvironmentFactory, GlobalPropertiesAccessor, NodeRuntimeEnvironmentFactory,
|
||||
BrowserRuntimeEnvironmentFactory, NodeRuntimeEnvironmentFactory,
|
||||
determineAndCreateRuntimeEnvironment,
|
||||
} from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
|
||||
import { ElectronEnvironmentDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
|
||||
import { ElectronEnvironmentDetectorStub } from '@tests/unit/shared/Stubs/ElectronEnvironmentDetectorStub';
|
||||
|
||||
describe('RuntimeEnvironmentFactory', () => {
|
||||
describe('determineAndCreateRuntimeEnvironment', () => {
|
||||
describe('Node environment creation', () => {
|
||||
it('selects Node environment if Electron main process detected', () => {
|
||||
it('creates Node environment in Electron main process', () => {
|
||||
// arrange
|
||||
const processStub = createProcessStub({
|
||||
versions: {
|
||||
electron: '28.1.3',
|
||||
} as NodeJS.ProcessVersions,
|
||||
});
|
||||
const expectedEnvironment = new RuntimeEnvironmentStub();
|
||||
const mainProcessDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('main');
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withGlobalProcess(processStub)
|
||||
.withElectronEnvironmentDetector(mainProcessDetector)
|
||||
.withNodeEnvironmentFactory(() => expectedEnvironment);
|
||||
// act
|
||||
const actualEnvironment = context.buildEnvironment();
|
||||
// assert
|
||||
expect(actualEnvironment).to.equal(expectedEnvironment);
|
||||
});
|
||||
it('passes correct process to Node environment factory', () => {
|
||||
// arrange
|
||||
const expectedProcess = createProcessStub({
|
||||
versions: {
|
||||
electron: '28.1.3',
|
||||
} as NodeJS.ProcessVersions,
|
||||
});
|
||||
let actualProcess: GlobalProcess;
|
||||
const nodeEnvironmentFactoryMock: NodeRuntimeEnvironmentFactory = (providedProcess) => {
|
||||
actualProcess = providedProcess;
|
||||
return new RuntimeEnvironmentStub();
|
||||
};
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withGlobalProcess(expectedProcess)
|
||||
.withNodeEnvironmentFactory(nodeEnvironmentFactoryMock);
|
||||
// act
|
||||
context.buildEnvironment();
|
||||
// assert
|
||||
expect(actualProcess).to.equal(expectedProcess);
|
||||
});
|
||||
});
|
||||
describe('browser environment creation', () => {
|
||||
it('selects browser environment if Electron main process not detected', () => {
|
||||
it('creates browser environment in Electron renderer process', () => {
|
||||
// arrange
|
||||
const expectedEnvironment = new RuntimeEnvironmentStub();
|
||||
const undefinedProcess: GlobalProcess = undefined;
|
||||
const rendererProcessDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('renderer');
|
||||
const windowStub = createWindowStub();
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withGlobalProcess(undefinedProcess)
|
||||
.withElectronEnvironmentDetector(rendererProcessDetector)
|
||||
.withGlobalWindow(windowStub)
|
||||
.withBrowserEnvironmentFactory(() => expectedEnvironment);
|
||||
// act
|
||||
@@ -60,21 +40,37 @@ describe('RuntimeEnvironmentFactory', () => {
|
||||
// assert
|
||||
expect(actualEnvironment).to.equal(expectedEnvironment);
|
||||
});
|
||||
it('passes correct window to browser environment factory', () => {
|
||||
it('creates browser environment in Electron preloader process', () => {
|
||||
// arrange
|
||||
const expectedEnvironment = new RuntimeEnvironmentStub();
|
||||
const preloaderProcessDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('preloader');
|
||||
const windowStub = createWindowStub();
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withElectronEnvironmentDetector(preloaderProcessDetector)
|
||||
.withGlobalWindow(windowStub)
|
||||
.withBrowserEnvironmentFactory(() => expectedEnvironment);
|
||||
// act
|
||||
const actualEnvironment = context.buildEnvironment();
|
||||
// assert
|
||||
expect(actualEnvironment).to.equal(expectedEnvironment);
|
||||
});
|
||||
it('provides correct window to browser environment factory', () => {
|
||||
// arrange
|
||||
const expectedWindow = createWindowStub({
|
||||
isRunningAsDesktopApplication: undefined,
|
||||
});
|
||||
let actualWindow: GlobalWindow;
|
||||
let actualWindow: Window | undefined;
|
||||
const browserEnvironmentFactoryMock
|
||||
: BrowserRuntimeEnvironmentFactory = (providedWindow) => {
|
||||
actualWindow = providedWindow;
|
||||
return new RuntimeEnvironmentStub();
|
||||
};
|
||||
const undefinedProcess: GlobalProcess = undefined;
|
||||
const nonElectronDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withNonElectronEnvironment();
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withGlobalWindow(expectedWindow)
|
||||
.withGlobalProcess(undefinedProcess)
|
||||
.withElectronEnvironmentDetector(nonElectronDetector)
|
||||
.withBrowserEnvironmentFactory(browserEnvironmentFactoryMock);
|
||||
// act
|
||||
context.buildEnvironment();
|
||||
@@ -82,14 +78,15 @@ describe('RuntimeEnvironmentFactory', () => {
|
||||
expect(actualWindow).to.equal(expectedWindow);
|
||||
});
|
||||
});
|
||||
it('throws error when both window and process are undefined', () => {
|
||||
it('throws error without global window in non-Electron environment', () => {
|
||||
// arrange
|
||||
const undefinedWindow: GlobalWindow = undefined;
|
||||
const undefinedProcess: GlobalProcess = undefined;
|
||||
const expectedError = 'Unsupported runtime environment: The current context is neither a recognized browser nor a desktop environment.';
|
||||
const nullWindow = null;
|
||||
const nonElectronDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withNonElectronEnvironment();
|
||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||
.withGlobalProcess(undefinedProcess)
|
||||
.withGlobalWindow(undefinedWindow);
|
||||
.withElectronEnvironmentDetector(nonElectronDetector)
|
||||
.withGlobalWindow(nullWindow);
|
||||
// act
|
||||
const act = () => context.buildEnvironment();
|
||||
// assert
|
||||
@@ -104,16 +101,11 @@ function createWindowStub(partialWindowProperties?: Partial<Window>): Window {
|
||||
} as Window;
|
||||
}
|
||||
|
||||
function createProcessStub(partialProcessProperties?: Partial<NodeJS.Process>): NodeJS.Process {
|
||||
return {
|
||||
...partialProcessProperties,
|
||||
} as NodeJS.Process;
|
||||
}
|
||||
|
||||
export class RuntimeEnvironmentFactoryTestSetup {
|
||||
private globalWindow: GlobalWindow = createWindowStub();
|
||||
private globalWindow: Window | undefined | null = createWindowStub();
|
||||
|
||||
private globalProcess: GlobalProcess = createProcessStub();
|
||||
private electronEnvironmentDetector
|
||||
: ElectronEnvironmentDetector = new ElectronEnvironmentDetectorStub();
|
||||
|
||||
private browserEnvironmentFactory
|
||||
: BrowserRuntimeEnvironmentFactory = () => new RuntimeEnvironmentStub();
|
||||
@@ -121,13 +113,13 @@ export class RuntimeEnvironmentFactoryTestSetup {
|
||||
private nodeEnvironmentFactory
|
||||
: NodeRuntimeEnvironmentFactory = () => new RuntimeEnvironmentStub();
|
||||
|
||||
public withGlobalWindow(globalWindow: GlobalWindow): this {
|
||||
public withGlobalWindow(globalWindow: Window | undefined | null): this {
|
||||
this.globalWindow = globalWindow;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withGlobalProcess(globalProcess: GlobalProcess): this {
|
||||
this.globalProcess = globalProcess;
|
||||
public withElectronEnvironmentDetector(detector: ElectronEnvironmentDetector): this {
|
||||
this.electronEnvironmentDetector = detector;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -147,16 +139,10 @@ export class RuntimeEnvironmentFactoryTestSetup {
|
||||
|
||||
public buildEnvironment(): ReturnType<typeof determineAndCreateRuntimeEnvironment> {
|
||||
return determineAndCreateRuntimeEnvironment(
|
||||
{
|
||||
window: this.globalWindow,
|
||||
process: this.globalProcess,
|
||||
},
|
||||
this.globalWindow,
|
||||
this.electronEnvironmentDetector,
|
||||
this.browserEnvironmentFactory,
|
||||
this.nodeEnvironmentFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type GlobalWindow = GlobalPropertiesAccessor['window'];
|
||||
|
||||
type GlobalProcess = GlobalPropertiesAccessor['process'];
|
||||
|
||||
@@ -2,222 +2,238 @@ import { describe, it, expect } from 'vitest';
|
||||
import { validateWindowVariables } from '@/infrastructure/WindowVariables/WindowVariablesValidator';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { getAbsentObjectTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { WindowVariablesStub } from '@tests/unit/shared/Stubs/WindowVariablesStub';
|
||||
import { CodeRunnerStub } from '@tests/unit/shared/Stubs/CodeRunnerStub';
|
||||
import { PropertyKeys } from '@/TypeHelpers';
|
||||
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
||||
import { DialogStub } from '@tests/unit/shared/Stubs/DialogStub';
|
||||
import { ScriptDiagnosticsCollectorStub } from '@tests/unit/shared/Stubs/ScriptDiagnosticsCollectorStub';
|
||||
import { ElectronEnvironmentDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
|
||||
import { ElectronEnvironmentDetectorStub } from '@tests/unit/shared/Stubs/ElectronEnvironmentDetectorStub';
|
||||
|
||||
describe('WindowVariablesValidator', () => {
|
||||
describe('validateWindowVariables', () => {
|
||||
describe('validates window type', () => {
|
||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
||||
// arrange
|
||||
const expectedError = 'window is not an object';
|
||||
const window: Partial<WindowVariables> = invalidObjectValue as never;
|
||||
// act
|
||||
const act = () => validateWindowVariables(window);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('property validations', () => {
|
||||
it('throws an error with a description of all invalid properties', () => {
|
||||
// arrange
|
||||
const invalidOs = 'invalid' as unknown as OperatingSystem;
|
||||
const invalidIsRunningAsDesktopApplication = 'not a boolean' as never;
|
||||
const expectedError = getExpectedError(
|
||||
{
|
||||
name: 'os',
|
||||
object: invalidOs,
|
||||
},
|
||||
{
|
||||
name: 'isRunningAsDesktopApplication',
|
||||
object: invalidIsRunningAsDesktopApplication,
|
||||
},
|
||||
);
|
||||
const input = new WindowVariablesStub()
|
||||
.withOs(invalidOs)
|
||||
.withIsRunningAsDesktopApplication(invalidIsRunningAsDesktopApplication);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
|
||||
describe('`os` property', () => {
|
||||
it('throws an error when os is not a number', () => {
|
||||
// arrange
|
||||
const invalidOs = 'Linux' as unknown as OperatingSystem;
|
||||
const expectedError = getExpectedError(
|
||||
{
|
||||
name: 'os',
|
||||
object: invalidOs,
|
||||
},
|
||||
);
|
||||
const input = new WindowVariablesStub()
|
||||
.withOs(invalidOs);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
|
||||
it('throws an error for an invalid numeric os value', () => {
|
||||
// arrange
|
||||
const invalidOs = Number.MAX_SAFE_INTEGER;
|
||||
const expectedError = getExpectedError(
|
||||
{
|
||||
name: 'os',
|
||||
object: invalidOs,
|
||||
},
|
||||
);
|
||||
const input = new WindowVariablesStub()
|
||||
.withOs(invalidOs);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
|
||||
it('does not throw for a missing os value', () => {
|
||||
// arrange
|
||||
const input = new WindowVariablesStub()
|
||||
.withIsRunningAsDesktopApplication(true)
|
||||
.withOs(undefined);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
|
||||
describe('`isRunningAsDesktopApplication` property', () => {
|
||||
it('does not throw when true with valid services', () => {
|
||||
// arrange
|
||||
const windowVariables = new WindowVariablesStub();
|
||||
const windowVariableConfigurators: Record< // Ensure types match for compile-time checking
|
||||
PropertyKeys<Required<WindowVariables>>,
|
||||
(stub: WindowVariablesStub) => WindowVariablesStub> = {
|
||||
isRunningAsDesktopApplication: (s) => s.withIsRunningAsDesktopApplication(true),
|
||||
codeRunner: (s) => s.withCodeRunner(new CodeRunnerStub()),
|
||||
os: (s) => s.withOs(OperatingSystem.Windows),
|
||||
log: (s) => s.withLog(new LoggerStub()),
|
||||
dialog: (s) => s.withDialog(new DialogStub()),
|
||||
};
|
||||
Object
|
||||
.values(windowVariableConfigurators)
|
||||
.forEach((configure) => configure(windowVariables));
|
||||
// act
|
||||
const act = () => validateWindowVariables(windowVariables);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
|
||||
describe('does not throw when false without services', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const absentCodeRunner = absentValue;
|
||||
const input = new WindowVariablesStub()
|
||||
.withIsRunningAsDesktopApplication(undefined)
|
||||
.withCodeRunner(absentCodeRunner);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('`codeRunner` property', () => {
|
||||
expectObjectOnDesktop('codeRunner');
|
||||
});
|
||||
|
||||
describe('`log` property', () => {
|
||||
expectObjectOnDesktop('log');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not throw for a valid object', () => {
|
||||
const input = new WindowVariablesStub();
|
||||
it('throws an error with a description of all invalid properties', () => {
|
||||
// arrange
|
||||
const invalidOs = 'invalid' as unknown as OperatingSystem;
|
||||
const invalidIsRunningAsDesktopApplication = 'not a boolean' as never;
|
||||
const expectedError = getExpectedError(
|
||||
{
|
||||
name: 'os',
|
||||
value: invalidOs,
|
||||
},
|
||||
{
|
||||
name: 'isRunningAsDesktopApplication',
|
||||
value: invalidIsRunningAsDesktopApplication,
|
||||
},
|
||||
);
|
||||
const input = new WindowVariablesStub()
|
||||
.withOs(invalidOs)
|
||||
.withIsRunningAsDesktopApplication(invalidIsRunningAsDesktopApplication);
|
||||
const context = new ValidateWindowVariablesTestSetup()
|
||||
.withWindowVariables(input);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
const act = () => context.validateWindowVariables();
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('when not in Electron renderer process', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly environment: ElectronEnvironmentDetector;
|
||||
}> = [
|
||||
{
|
||||
description: 'skips in non-Electron environments',
|
||||
environment: new ElectronEnvironmentDetectorStub()
|
||||
.withNonElectronEnvironment(),
|
||||
},
|
||||
{
|
||||
description: 'skips in Electron main process',
|
||||
environment: new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('main'),
|
||||
},
|
||||
{
|
||||
description: 'skips in Electron preloader process',
|
||||
environment: new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('preloader'),
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({ description, environment }) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const invalidOs = 'invalid' as unknown as OperatingSystem;
|
||||
const input = new WindowVariablesStub()
|
||||
.withOs(invalidOs);
|
||||
const context = new ValidateWindowVariablesTestSetup()
|
||||
.withElectronDetector(environment)
|
||||
.withWindowVariables(input);
|
||||
// act
|
||||
const act = () => context.validateWindowVariables();
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('does not throw when a property is valid', () => {
|
||||
const testScenarios: Record<PropertyKeys<Required<WindowVariables>>, ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly validValue: unknown;
|
||||
}>> = {
|
||||
isRunningAsDesktopApplication: [{
|
||||
description: 'accepts boolean true',
|
||||
validValue: true,
|
||||
}],
|
||||
os: [
|
||||
{
|
||||
description: 'accepts undefined',
|
||||
validValue: undefined,
|
||||
},
|
||||
{
|
||||
description: 'accepts valid enum value',
|
||||
validValue: OperatingSystem.WindowsPhone,
|
||||
},
|
||||
],
|
||||
codeRunner: [{
|
||||
description: 'accepts an object',
|
||||
validValue: new CodeRunnerStub(),
|
||||
}],
|
||||
log: [{
|
||||
description: 'accepts an object',
|
||||
validValue: new LoggerStub(),
|
||||
}],
|
||||
dialog: [{
|
||||
description: 'accepts an object',
|
||||
validValue: new DialogStub(),
|
||||
}],
|
||||
scriptDiagnosticsCollector: [{
|
||||
description: 'accepts an object',
|
||||
validValue: new ScriptDiagnosticsCollectorStub(),
|
||||
}],
|
||||
};
|
||||
Object.entries(testScenarios).forEach(([propertyKey, validValueScenarios]) => {
|
||||
describe(propertyKey, () => {
|
||||
validValueScenarios.forEach(({ description, validValue }) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const input = new WindowVariablesStub();
|
||||
input[propertyKey] = validValue;
|
||||
const context = new ValidateWindowVariablesTestSetup()
|
||||
.withWindowVariables(input);
|
||||
// act
|
||||
const act = () => context.validateWindowVariables();
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('throws an error when a property is invalid', () => {
|
||||
interface InvalidValueTestCase {
|
||||
readonly description: string;
|
||||
readonly invalidValue: unknown;
|
||||
}
|
||||
const testScenarios: Record<
|
||||
PropertyKeys<Required<WindowVariables>>,
|
||||
ReadonlyArray<InvalidValueTestCase>> = {
|
||||
isRunningAsDesktopApplication: [
|
||||
{
|
||||
description: 'rejects false',
|
||||
invalidValue: false,
|
||||
},
|
||||
{
|
||||
description: 'rejects undefined',
|
||||
invalidValue: undefined,
|
||||
},
|
||||
],
|
||||
os: [
|
||||
{
|
||||
description: 'rejects non-numeric',
|
||||
invalidValue: 'Linux',
|
||||
},
|
||||
{
|
||||
description: 'rejects out-of-range',
|
||||
invalidValue: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
],
|
||||
codeRunner: getInvalidObjectValueTestCases(),
|
||||
log: getInvalidObjectValueTestCases(),
|
||||
dialog: getInvalidObjectValueTestCases(),
|
||||
scriptDiagnosticsCollector: getInvalidObjectValueTestCases(),
|
||||
};
|
||||
Object.entries(testScenarios).forEach(([propertyKey, validValueScenarios]) => {
|
||||
describe(propertyKey, () => {
|
||||
validValueScenarios.forEach(({ description, invalidValue }) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const expectedErrorMessage = getExpectedError({
|
||||
name: propertyKey as keyof WindowVariables,
|
||||
value: invalidValue,
|
||||
});
|
||||
const input = new WindowVariablesStub();
|
||||
input[propertyKey] = invalidValue;
|
||||
const context = new ValidateWindowVariablesTestSetup()
|
||||
.withWindowVariables(input);
|
||||
// act
|
||||
const act = () => context.validateWindowVariables();
|
||||
// assert
|
||||
expect(act).to.throw(expectedErrorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
function getInvalidObjectValueTestCases(): InvalidValueTestCase[] {
|
||||
return [
|
||||
{
|
||||
description: 'rejects string',
|
||||
invalidValue: 'invalid object',
|
||||
},
|
||||
{
|
||||
description: 'rejects array of objects',
|
||||
invalidValue: [{}, {}],
|
||||
},
|
||||
...getAbsentObjectTestCases().map((testCase) => ({
|
||||
description: `rejects absent: ${testCase.valueName}`,
|
||||
invalidValue: testCase.absentValue,
|
||||
})),
|
||||
];
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectObjectOnDesktop<T>(key: keyof WindowVariables) {
|
||||
describe('validates object type on desktop', () => {
|
||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
||||
// arrange
|
||||
const isOnDesktop = true;
|
||||
const invalidObject = invalidObjectValue as T;
|
||||
const expectedError = getExpectedError({
|
||||
name: key,
|
||||
object: invalidObject,
|
||||
});
|
||||
const input: WindowVariables = {
|
||||
...new WindowVariablesStub(),
|
||||
isRunningAsDesktopApplication: isOnDesktop,
|
||||
[key]: invalidObject,
|
||||
};
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('does not validate object type when not on desktop', () => {
|
||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
||||
// arrange
|
||||
const invalidObject = invalidObjectValue as T;
|
||||
const input: WindowVariables = {
|
||||
...new WindowVariablesStub(),
|
||||
isRunningAsDesktopApplication: undefined,
|
||||
[key]: invalidObject,
|
||||
};
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
});
|
||||
}
|
||||
class ValidateWindowVariablesTestSetup {
|
||||
private electronDetector: ElectronEnvironmentDetector = new ElectronEnvironmentDetectorStub()
|
||||
.withElectronEnvironment('renderer');
|
||||
|
||||
function itEachInvalidObjectValue<T>(runner: (invalidObjectValue: T) => void) {
|
||||
const testCases: Array<{
|
||||
readonly name: string;
|
||||
readonly value: T;
|
||||
}> = [
|
||||
{
|
||||
name: 'given string',
|
||||
value: 'invalid object' as unknown as T,
|
||||
},
|
||||
{
|
||||
name: 'given array of objects',
|
||||
value: [{}, {}] as unknown as T,
|
||||
},
|
||||
...getAbsentObjectTestCases().map((testCase) => ({
|
||||
name: `given absent: ${testCase.valueName}`,
|
||||
value: testCase.absentValue as unknown as T,
|
||||
})),
|
||||
];
|
||||
testCases.forEach((testCase) => {
|
||||
it(testCase.name, () => {
|
||||
runner(testCase.value);
|
||||
});
|
||||
});
|
||||
private windowVariables: WindowVariables = new WindowVariablesStub();
|
||||
|
||||
public withWindowVariables(windowVariables: WindowVariables): this {
|
||||
this.windowVariables = windowVariables;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withElectronDetector(electronDetector: ElectronEnvironmentDetector): this {
|
||||
this.electronDetector = electronDetector;
|
||||
return this;
|
||||
}
|
||||
|
||||
public validateWindowVariables() {
|
||||
return validateWindowVariables(
|
||||
this.windowVariables,
|
||||
this.electronDetector,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getExpectedError(...unexpectedObjects: Array<{
|
||||
readonly name: keyof WindowVariables;
|
||||
readonly object: unknown;
|
||||
readonly value: unknown;
|
||||
}>) {
|
||||
const errors = unexpectedObjects
|
||||
.map(({ name, object }) => `Unexpected ${name} (${typeof object})`);
|
||||
.map(({ name, value: object }) => `Unexpected ${name} (${typeof object})`);
|
||||
return errors.join('\n');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ScriptDirectoryProvider } from '@/infrastructure/CodeRunner/Creation/Directory/ScriptDirectoryProvider';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { ScriptEnvironmentDiagnosticsCollector } from '@/infrastructure/ScriptDiagnostics/ScriptEnvironmentDiagnosticsCollector';
|
||||
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
|
||||
import { ScriptDirectoryProviderStub } from '@tests/unit/shared/Stubs/ScriptDirectoryProviderStub';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
describe('ScriptEnvironmentDiagnosticsCollector', () => {
|
||||
it('collects operating system path correctly', async () => {
|
||||
// arrange
|
||||
const expectedOperatingSystem = OperatingSystem.KaiOS;
|
||||
const environment = new RuntimeEnvironmentStub()
|
||||
.withOs(expectedOperatingSystem);
|
||||
const collector = new CollectorBuilder()
|
||||
.withEnvironment(environment)
|
||||
.build();
|
||||
|
||||
// act
|
||||
const diagnosticData = await collector.collectDiagnosticInformation();
|
||||
|
||||
// assert
|
||||
const actualOperatingSystem = diagnosticData.currentOperatingSystem;
|
||||
expect(actualOperatingSystem).to.equal(expectedOperatingSystem);
|
||||
});
|
||||
it('collects path correctly', async () => {
|
||||
// arrange
|
||||
const expectedScriptsPath = '/expected/scripts/path';
|
||||
const directoryProvider = new ScriptDirectoryProviderStub()
|
||||
.withDirectoryPath(expectedScriptsPath);
|
||||
const collector = new CollectorBuilder()
|
||||
.withScriptDirectoryProvider(directoryProvider)
|
||||
.build();
|
||||
|
||||
// act
|
||||
const diagnosticData = await collector.collectDiagnosticInformation();
|
||||
|
||||
// assert
|
||||
const actualScriptsPath = diagnosticData.scriptsDirectoryAbsolutePath;
|
||||
expect(actualScriptsPath).to.equal(expectedScriptsPath);
|
||||
});
|
||||
});
|
||||
|
||||
class CollectorBuilder {
|
||||
private directoryProvider: ScriptDirectoryProvider = new ScriptDirectoryProviderStub();
|
||||
|
||||
private environment: RuntimeEnvironment = new RuntimeEnvironmentStub();
|
||||
|
||||
public withEnvironment(environment: RuntimeEnvironment): this {
|
||||
this.environment = environment;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScriptDirectoryProvider(directoryProvider: ScriptDirectoryProvider): this {
|
||||
this.directoryProvider = directoryProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build() {
|
||||
return new ScriptEnvironmentDiagnosticsCollector(
|
||||
this.directoryProvider,
|
||||
this.environment,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ describe('DependencyProvider', () => {
|
||||
useLogger: createTransientTests(),
|
||||
useCodeRunner: createTransientTests(),
|
||||
useDialog: createTransientTests(),
|
||||
useScriptDiagnosticsCollector: createTransientTests(),
|
||||
};
|
||||
Object.entries(testCases).forEach(([key, runTests]) => {
|
||||
const registeredKey = InjectionKeys[key].key;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { ScriptErrorDetails, createScriptErrorDialog } from '@/presentation/components/Code/CodeButtons/ScriptErrorDialog';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { AllSupportedOperatingSystems } from '@tests/shared/TestCases/SupportedOperatingSystems';
|
||||
import { ScriptDiagnosticsCollectorStub } from '@tests/unit/shared/Stubs/ScriptDiagnosticsCollectorStub';
|
||||
|
||||
describe('ScriptErrorDialog', () => {
|
||||
describe('handles readback error type', () => {
|
||||
it('handles file readback error', async () => {
|
||||
// arrange
|
||||
const errorDetails = createErrorDetails({ isFileReadbackError: true });
|
||||
const context = new CreateScriptErrorDialogTestSetup()
|
||||
.withDetails(errorDetails);
|
||||
// act
|
||||
const dialog = await context.createScriptErrorDialog();
|
||||
// assert
|
||||
assertValidDialog(dialog);
|
||||
});
|
||||
it('handles generic error', async () => {
|
||||
// arrange
|
||||
const errorDetails = createErrorDetails({ isFileReadbackError: false });
|
||||
const context = new CreateScriptErrorDialogTestSetup()
|
||||
.withDetails(errorDetails);
|
||||
// act
|
||||
const dialog = await context.createScriptErrorDialog();
|
||||
// assert
|
||||
assertValidDialog(dialog);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles supported operatingSystems', () => {
|
||||
AllSupportedOperatingSystems.forEach((operatingSystem) => {
|
||||
it(`${OperatingSystem[operatingSystem]}`, async () => {
|
||||
// arrange
|
||||
const diagnostics = new ScriptDiagnosticsCollectorStub()
|
||||
.withOperatingSystem(operatingSystem);
|
||||
const context = new CreateScriptErrorDialogTestSetup()
|
||||
.withDiagnostics(diagnostics);
|
||||
// act
|
||||
const dialog = await context.createScriptErrorDialog();
|
||||
// assert
|
||||
assertValidDialog(dialog);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles undefined diagnostics collector', async () => {
|
||||
const diagnostics = undefined;
|
||||
const context = new CreateScriptErrorDialogTestSetup()
|
||||
.withDiagnostics(diagnostics);
|
||||
// act
|
||||
const dialog = await context.createScriptErrorDialog();
|
||||
// assert
|
||||
assertValidDialog(dialog);
|
||||
});
|
||||
|
||||
it('handles undefined operating system', async () => {
|
||||
// arrange
|
||||
const undefinedOperatingSystem = undefined;
|
||||
const diagnostics = new ScriptDiagnosticsCollectorStub()
|
||||
.withOperatingSystem(undefinedOperatingSystem);
|
||||
const context = new CreateScriptErrorDialogTestSetup()
|
||||
.withDiagnostics(diagnostics);
|
||||
// act
|
||||
const dialog = await context.createScriptErrorDialog();
|
||||
// assert
|
||||
assertValidDialog(dialog);
|
||||
});
|
||||
|
||||
it('handles directory path', async () => {
|
||||
// arrange
|
||||
const undefinedScriptsDirectory = undefined;
|
||||
const diagnostics = new ScriptDiagnosticsCollectorStub()
|
||||
.withScriptDirectoryPath(undefinedScriptsDirectory);
|
||||
const context = new CreateScriptErrorDialogTestSetup()
|
||||
.withDiagnostics(diagnostics);
|
||||
// act
|
||||
const dialog = await context.createScriptErrorDialog();
|
||||
// assert
|
||||
assertValidDialog(dialog);
|
||||
});
|
||||
|
||||
describe('handles all contexts', () => {
|
||||
const possibleContexts: ScriptErrorDetails['errorContext'][] = ['run', 'save'];
|
||||
possibleContexts.forEach((dialogContext) => {
|
||||
it(`${dialogContext} context`, async () => {
|
||||
// arrange
|
||||
const undefinedScriptsDirectory = undefined;
|
||||
const diagnostics = new ScriptDiagnosticsCollectorStub()
|
||||
.withScriptDirectoryPath(undefinedScriptsDirectory);
|
||||
const context = new CreateScriptErrorDialogTestSetup()
|
||||
.withDiagnostics(diagnostics);
|
||||
// act
|
||||
const dialog = await context.createScriptErrorDialog();
|
||||
// assert
|
||||
assertValidDialog(dialog);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function assertValidDialog(dialog: Parameters<Dialog['showError']>): void {
|
||||
expectExists(dialog);
|
||||
const [title, message] = dialog;
|
||||
expectExists(title);
|
||||
expect(title).to.have.length.greaterThan(1);
|
||||
expectExists(message);
|
||||
expect(message).to.have.length.greaterThan(1);
|
||||
}
|
||||
|
||||
function createErrorDetails(partialDetails?: Partial<ScriptErrorDetails>): ScriptErrorDetails {
|
||||
const defaultDetails: ScriptErrorDetails = {
|
||||
errorContext: 'run',
|
||||
errorType: 'test-error-type',
|
||||
errorMessage: 'test error message',
|
||||
isFileReadbackError: false,
|
||||
};
|
||||
return {
|
||||
...defaultDetails,
|
||||
...partialDetails,
|
||||
};
|
||||
}
|
||||
|
||||
class CreateScriptErrorDialogTestSetup {
|
||||
private details: ScriptErrorDetails = createErrorDetails();
|
||||
|
||||
private diagnostics:
|
||||
ScriptDiagnosticsCollector | undefined = new ScriptDiagnosticsCollectorStub();
|
||||
|
||||
public withDetails(details: ScriptErrorDetails): this {
|
||||
this.details = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDiagnostics(diagnostics: ScriptDiagnosticsCollector | undefined): this {
|
||||
this.diagnostics = diagnostics;
|
||||
return this;
|
||||
}
|
||||
|
||||
public createScriptErrorDialog() {
|
||||
return createScriptErrorDialog(
|
||||
this.details,
|
||||
this.diagnostics,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { useScriptDiagnosticsCollector } from '@/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector';
|
||||
import { ScriptDiagnosticsCollectorStub } from '@tests/unit/shared/Stubs/ScriptDiagnosticsCollectorStub';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
|
||||
describe('useScriptDiagnosticsCollector', () => {
|
||||
it('returns undefined if collector is not present on the window object', () => {
|
||||
// arrange
|
||||
const emptyWindow = {} as WindowVariables;
|
||||
// act
|
||||
const { scriptDiagnosticsCollector } = useScriptDiagnosticsCollector(emptyWindow);
|
||||
// assert
|
||||
expect(scriptDiagnosticsCollector).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('returns the scriptDiagnosticsCollector when it is present on the window object', () => {
|
||||
// arrange
|
||||
const expectedCollector = new ScriptDiagnosticsCollectorStub();
|
||||
const windowWithVariable: Partial<WindowVariables> = {
|
||||
scriptDiagnosticsCollector: expectedCollector,
|
||||
} as Partial<WindowVariables>;
|
||||
// act
|
||||
const { scriptDiagnosticsCollector } = useScriptDiagnosticsCollector(windowWithVariable);
|
||||
// assert
|
||||
expect(scriptDiagnosticsCollector).to.equal(expectedCollector);
|
||||
});
|
||||
});
|
||||
@@ -2,12 +2,14 @@ import { describe, it, expect } from 'vitest';
|
||||
import { CodeRunnerStub } from '@tests/unit/shared/Stubs/CodeRunnerStub';
|
||||
import { ChannelDefinitionKey, IpcChannelDefinitions } from '@/presentation/electron/shared/IpcBridging/IpcChannelDefinitions';
|
||||
import {
|
||||
CodeRunnerFactory, DialogFactory, IpcChannelRegistrar, registerAllIpcChannels,
|
||||
CodeRunnerFactory, DialogFactory, IpcChannelRegistrar,
|
||||
ScriptDiagnosticsCollectorFactory, registerAllIpcChannels,
|
||||
} from '@/presentation/electron/main/IpcRegistration';
|
||||
import { IpcChannel } from '@/presentation/electron/shared/IpcBridging/IpcChannel';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||
import { DialogStub } from '@tests/unit/shared/Stubs/DialogStub';
|
||||
import { ScriptDiagnosticsCollectorStub } from '../../../shared/Stubs/ScriptDiagnosticsCollectorStub';
|
||||
|
||||
describe('IpcRegistration', () => {
|
||||
describe('registerAllIpcChannels', () => {
|
||||
@@ -44,6 +46,13 @@ describe('IpcRegistration', () => {
|
||||
expectedInstance,
|
||||
};
|
||||
})(),
|
||||
ScriptDiagnosticsCollector: (() => {
|
||||
const expectedInstance = new ScriptDiagnosticsCollectorStub();
|
||||
return {
|
||||
buildContext: (c) => c.witScriptDiagnosticsCollectorFactory(() => expectedInstance),
|
||||
expectedInstance,
|
||||
};
|
||||
})(),
|
||||
};
|
||||
Object.entries(testScenarios).forEach(([
|
||||
key, { buildContext, expectedInstance },
|
||||
@@ -79,11 +88,14 @@ describe('IpcRegistration', () => {
|
||||
});
|
||||
|
||||
class IpcRegistrationTestSetup {
|
||||
private registrar: IpcChannelRegistrar = () => { /* NOOP */ };
|
||||
|
||||
private codeRunnerFactory: CodeRunnerFactory = () => new CodeRunnerStub();
|
||||
|
||||
private dialogFactory: DialogFactory = () => new DialogStub();
|
||||
|
||||
private registrar: IpcChannelRegistrar = () => { /* NOOP */ };
|
||||
private scriptDiagnosticsCollectorFactory
|
||||
: ScriptDiagnosticsCollectorFactory = () => new ScriptDiagnosticsCollectorStub();
|
||||
|
||||
public withRegistrar(registrar: IpcChannelRegistrar): this {
|
||||
this.registrar = registrar;
|
||||
@@ -100,11 +112,19 @@ class IpcRegistrationTestSetup {
|
||||
return this;
|
||||
}
|
||||
|
||||
public witScriptDiagnosticsCollectorFactory(
|
||||
scriptDiagnosticsCollectorFactory: ScriptDiagnosticsCollectorFactory,
|
||||
): this {
|
||||
this.scriptDiagnosticsCollectorFactory = scriptDiagnosticsCollectorFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public run() {
|
||||
registerAllIpcChannels(
|
||||
this.registrar,
|
||||
this.codeRunnerFactory,
|
||||
this.dialogFactory,
|
||||
this.registrar,
|
||||
this.scriptDiagnosticsCollectorFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ describe('RendererApiProvider', () => {
|
||||
setupContext(context: RendererApiProviderTestContext): RendererApiProviderTestContext;
|
||||
readonly expectedValue: unknown;
|
||||
}
|
||||
const testScenarios: Record<PropertyKeys<Required<WindowVariables>>, WindowVariableTestCase> = {
|
||||
const testScenarios: Record<
|
||||
PropertyKeys<Required<WindowVariables>>,
|
||||
WindowVariableTestCase> = {
|
||||
isRunningAsDesktopApplication: {
|
||||
description: 'returns true',
|
||||
setupContext: (context) => context,
|
||||
@@ -32,9 +34,12 @@ describe('RendererApiProvider', () => {
|
||||
})(),
|
||||
log: expectFacade({
|
||||
instance: new LoggerStub(),
|
||||
setupContext: (c, logger) => c.withLogger(logger),
|
||||
setupContext: (c, instance) => c.withLogger(instance),
|
||||
}),
|
||||
dialog: expectIpcConsumer(IpcChannelDefinitions.Dialog),
|
||||
scriptDiagnosticsCollector: expectIpcConsumer(
|
||||
IpcChannelDefinitions.ScriptDiagnosticsCollector,
|
||||
),
|
||||
};
|
||||
Object.entries(testScenarios).forEach((
|
||||
[property, { description, setupContext, expectedValue }],
|
||||
@@ -109,10 +114,10 @@ class RendererApiProviderTestContext {
|
||||
|
||||
public provideWindowVariables() {
|
||||
return provideWindowVariables(
|
||||
() => this.log,
|
||||
() => this.os,
|
||||
this.apiFacadeCreator,
|
||||
this.ipcConsumerCreator,
|
||||
() => this.os,
|
||||
() => this.log,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ describe('IpcChannelDefinitions', () => {
|
||||
expectedNamespace: 'dialogs',
|
||||
expectedAccessibleMembers: ['saveFile'],
|
||||
},
|
||||
ScriptDiagnosticsCollector: {
|
||||
expectedNamespace: 'script-diagnostics-collector',
|
||||
expectedAccessibleMembers: ['collectDiagnosticInformation'],
|
||||
},
|
||||
};
|
||||
Object.entries(testScenarios).forEach((
|
||||
[definitionKey, { expectedNamespace, expectedAccessibleMembers }],
|
||||
|
||||
26
tests/unit/shared/Stubs/ElectronEnvironmentDetectorStub.ts
Normal file
26
tests/unit/shared/Stubs/ElectronEnvironmentDetectorStub.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ElectronEnvironmentDetector, ElectronProcessType } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
|
||||
|
||||
export class ElectronEnvironmentDetectorStub implements ElectronEnvironmentDetector {
|
||||
private isInsideElectron = true;
|
||||
|
||||
public process: ElectronProcessType = 'renderer';
|
||||
|
||||
public isRunningInsideElectron(): boolean {
|
||||
return this.isInsideElectron;
|
||||
}
|
||||
|
||||
public determineElectronProcessType(): ElectronProcessType {
|
||||
return this.process;
|
||||
}
|
||||
|
||||
public withNonElectronEnvironment(): this {
|
||||
this.isInsideElectron = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withElectronEnvironment(process: ElectronProcessType): this {
|
||||
this.isInsideElectron = true;
|
||||
this.process = process;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
25
tests/unit/shared/Stubs/ScriptDiagnosticsCollectorStub.ts
Normal file
25
tests/unit/shared/Stubs/ScriptDiagnosticsCollectorStub.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ScriptDiagnosticData, ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export class ScriptDiagnosticsCollectorStub implements ScriptDiagnosticsCollector {
|
||||
private operatingSystem: OperatingSystem | undefined = OperatingSystem.Windows;
|
||||
|
||||
private scriptDirectoryPath: string | undefined = '/test/scripts/directory/path';
|
||||
|
||||
public withOperatingSystem(operatingSystem: OperatingSystem | undefined): this {
|
||||
this.operatingSystem = operatingSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withScriptDirectoryPath(scriptDirectoryPath: string | undefined): this {
|
||||
this.scriptDirectoryPath = scriptDirectoryPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public collectDiagnosticInformation(): Promise<ScriptDiagnosticData> {
|
||||
return Promise.resolve({
|
||||
scriptsDirectoryAbsolutePath: this.scriptDirectoryPath,
|
||||
currentOperatingSystem: this.operatingSystem,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@ import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
import { LoggerStub } from './LoggerStub';
|
||||
import { CodeRunnerStub } from './CodeRunnerStub';
|
||||
import { DialogStub } from './DialogStub';
|
||||
import { ScriptDiagnosticsCollectorStub } from './ScriptDiagnosticsCollectorStub';
|
||||
|
||||
export class WindowVariablesStub implements WindowVariables {
|
||||
public codeRunner?: CodeRunner = new CodeRunnerStub();
|
||||
@@ -18,6 +20,16 @@ export class WindowVariablesStub implements WindowVariables {
|
||||
|
||||
public dialog?: Dialog = new DialogStub();
|
||||
|
||||
public scriptDiagnosticsCollector?
|
||||
: ScriptDiagnosticsCollector = new ScriptDiagnosticsCollectorStub();
|
||||
|
||||
public withScriptDiagnosticsCollector(
|
||||
scriptDiagnosticsCollector: ScriptDiagnosticsCollector,
|
||||
): this {
|
||||
this.scriptDiagnosticsCollector = scriptDiagnosticsCollector;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLog(log: Logger): this {
|
||||
this.log = log;
|
||||
return this;
|
||||
|
||||
Reference in New Issue
Block a user