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
|
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
|
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.
|
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
|
### 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 |
|
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
||||||
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
|
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
|
||||||
| [Error handling](#error-handling) | 🟢 Advanced | 🟡 Limited |
|
| [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
|
## 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.
|
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.
|
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>> = [
|
const fileWritePipelineActions: ReadonlyArray<() => Promise<FileWriteOutcome>> = [
|
||||||
() => this.createOrOverwriteFile(filePath, fileContents),
|
() => this.createOrOverwriteFile(filePath, fileContents),
|
||||||
() => this.verifyFileExistsWithoutReading(filePath),
|
() => 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),
|
() => this.verifyFileContentsByReading(filePath, fileContents),
|
||||||
];
|
];
|
||||||
for (const action of fileWritePipelineActions) {
|
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 { BrowserRuntimeEnvironment } from './Browser/BrowserRuntimeEnvironment';
|
||||||
import { NodeRuntimeEnvironment } from './Node/NodeRuntimeEnvironment';
|
import { NodeRuntimeEnvironment } from './Node/NodeRuntimeEnvironment';
|
||||||
import { RuntimeEnvironment } from './RuntimeEnvironment';
|
import { RuntimeEnvironment } from './RuntimeEnvironment';
|
||||||
|
import { ContextIsolatedElectronDetector } from './Electron/ContextIsolatedElectronDetector';
|
||||||
|
|
||||||
export const CurrentEnvironment = determineAndCreateRuntimeEnvironment({
|
export const CurrentEnvironment = determineAndCreateRuntimeEnvironment(globalThis.window);
|
||||||
window: globalThis.window,
|
|
||||||
process: globalThis.process,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function determineAndCreateRuntimeEnvironment(
|
export function determineAndCreateRuntimeEnvironment(
|
||||||
globalAccessor: GlobalPropertiesAccessor,
|
globalWindow: Window | undefined | null = globalThis.window,
|
||||||
|
electronDetector: ElectronEnvironmentDetector = new ContextIsolatedElectronDetector(),
|
||||||
browserEnvironmentFactory: BrowserRuntimeEnvironmentFactory = (
|
browserEnvironmentFactory: BrowserRuntimeEnvironmentFactory = (
|
||||||
window,
|
window,
|
||||||
) => new BrowserRuntimeEnvironment(window),
|
) => new BrowserRuntimeEnvironment(window),
|
||||||
nodeEnvironmentFactory: NodeRuntimeEnvironmentFactory = (
|
nodeEnvironmentFactory: NodeRuntimeEnvironmentFactory = () => new NodeRuntimeEnvironment(),
|
||||||
process: NodeJS.Process,
|
|
||||||
) => new NodeRuntimeEnvironment(process),
|
|
||||||
): RuntimeEnvironment {
|
): RuntimeEnvironment {
|
||||||
if (isElectronMainProcess(globalAccessor.process)) {
|
if (
|
||||||
return nodeEnvironmentFactory(globalAccessor.process);
|
electronDetector.isRunningInsideElectron()
|
||||||
|
&& electronDetector.determineElectronProcessType() === 'main') {
|
||||||
|
return nodeEnvironmentFactory();
|
||||||
}
|
}
|
||||||
const { window } = globalAccessor;
|
if (!globalWindow) {
|
||||||
if (!window) {
|
|
||||||
throw new Error('Unsupported runtime environment: The current context is neither a recognized browser nor a desktop environment.');
|
throw new Error('Unsupported runtime environment: The current context is neither a recognized browser nor a desktop environment.');
|
||||||
}
|
}
|
||||||
return browserEnvironmentFactory(window);
|
return browserEnvironmentFactory(globalWindow);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BrowserRuntimeEnvironmentFactory = (window: Window) => RuntimeEnvironment;
|
export type BrowserRuntimeEnvironmentFactory = (window: Window) => RuntimeEnvironment;
|
||||||
|
|
||||||
export type NodeRuntimeEnvironmentFactory = (process: NodeJS.Process) => NodeRuntimeEnvironment;
|
export type NodeRuntimeEnvironmentFactory = () => NodeRuntimeEnvironment;
|
||||||
|
|
||||||
export interface GlobalPropertiesAccessor {
|
export type GlobalWindowAccessor = Window | undefined;
|
||||||
readonly window: Window | undefined;
|
|
||||||
readonly process: NodeJS.Process | 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 { Logger } from '@/application/Common/Log/Logger';
|
||||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||||
import { Dialog } from '@/presentation/common/Dialog';
|
import { Dialog } from '@/presentation/common/Dialog';
|
||||||
|
import { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||||
|
|
||||||
/* Primary entry point for platform-specific injections */
|
/* Primary entry point for platform-specific injections */
|
||||||
export interface WindowVariables {
|
export interface WindowVariables {
|
||||||
@@ -10,4 +11,5 @@ export interface WindowVariables {
|
|||||||
readonly os?: OperatingSystem;
|
readonly os?: OperatingSystem;
|
||||||
readonly log?: Logger;
|
readonly log?: Logger;
|
||||||
readonly dialog?: Dialog;
|
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 { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import {
|
import {
|
||||||
PropertyKeys, isBoolean, isFunction, isNumber, isPlainObject,
|
PropertyKeys, isBoolean, isFunction, isNumber, isPlainObject,
|
||||||
@@ -7,7 +9,14 @@ import { WindowVariables } from './WindowVariables';
|
|||||||
/**
|
/**
|
||||||
* Checks for consistency in runtime environment properties injected by Electron preloader.
|
* 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)) {
|
if (!isPlainObject(variables)) {
|
||||||
throw new Error('window is not an object');
|
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> {
|
function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<string> {
|
||||||
const tests: Record<PropertyKeys<Required<WindowVariables>>, boolean> = {
|
const tests: Record<PropertyKeys<Required<WindowVariables>>, boolean> = {
|
||||||
os: testOperatingSystem(variables.os),
|
os: testOperatingSystem(variables.os),
|
||||||
isRunningAsDesktopApplication: testIsRunningAsDesktopApplication(
|
isRunningAsDesktopApplication: testIsRunningAsDesktopApplication(variables),
|
||||||
variables.isRunningAsDesktopApplication,
|
|
||||||
),
|
|
||||||
codeRunner: testCodeRunner(variables),
|
codeRunner: testCodeRunner(variables),
|
||||||
log: testLogger(variables),
|
log: testLogger(variables),
|
||||||
dialog: testDialog(variables),
|
dialog: testDialog(variables),
|
||||||
|
scriptDiagnosticsCollector: testScriptDiagnosticsCollector(variables),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [propertyName, testResult] of Object.entries(tests)) {
|
for (const [propertyName, testResult] of Object.entries(tests)) {
|
||||||
@@ -49,30 +57,30 @@ function testOperatingSystem(os: unknown): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function testLogger(variables: Partial<WindowVariables>): boolean {
|
function testLogger(variables: Partial<WindowVariables>): boolean {
|
||||||
if (!variables.isRunningAsDesktopApplication) {
|
return isPlainObject(variables.log)
|
||||||
return true;
|
&& isFunction(variables.log.debug)
|
||||||
}
|
&& isFunction(variables.log.info)
|
||||||
return isPlainObject(variables.log);
|
&& isFunction(variables.log.error)
|
||||||
|
&& isFunction(variables.log.warn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCodeRunner(variables: Partial<WindowVariables>): boolean {
|
function testCodeRunner(variables: Partial<WindowVariables>): boolean {
|
||||||
if (!variables.isRunningAsDesktopApplication) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return isPlainObject(variables.codeRunner)
|
return isPlainObject(variables.codeRunner)
|
||||||
&& isFunction(variables.codeRunner.runCode);
|
&& isFunction(variables.codeRunner.runCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testIsRunningAsDesktopApplication(isRunningAsDesktopApplication: unknown): boolean {
|
function testIsRunningAsDesktopApplication(variables: Partial<WindowVariables>): boolean {
|
||||||
if (isRunningAsDesktopApplication === undefined) {
|
return isBoolean(variables.isRunningAsDesktopApplication)
|
||||||
return true;
|
&& variables.isRunningAsDesktopApplication === true;
|
||||||
}
|
|
||||||
return isBoolean(isRunningAsDesktopApplication);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDialog(variables: Partial<WindowVariables>): boolean {
|
function testDialog(variables: Partial<WindowVariables>): boolean {
|
||||||
if (!variables.isRunningAsDesktopApplication) {
|
return isPlainObject(variables.dialog)
|
||||||
return true;
|
&& isFunction(variables.dialog.saveFile)
|
||||||
}
|
&& isFunction(variables.dialog.showError);
|
||||||
return isPlainObject(variables.dialog);
|
}
|
||||||
|
|
||||||
|
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 { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRunner';
|
||||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||||
import { useDialog } from '@/presentation/components/Shared/Hooks/Dialog/UseDialog';
|
import { useDialog } from '@/presentation/components/Shared/Hooks/Dialog/UseDialog';
|
||||||
|
import { useScriptDiagnosticsCollector } from '@/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector';
|
||||||
|
|
||||||
export function provideDependencies(
|
export function provideDependencies(
|
||||||
context: IApplicationContext,
|
context: IApplicationContext,
|
||||||
@@ -72,6 +73,10 @@ export function provideDependencies(
|
|||||||
InjectionKeys.useDialog,
|
InjectionKeys.useDialog,
|
||||||
useDialog,
|
useDialog,
|
||||||
),
|
),
|
||||||
|
useScriptDiagnosticsCollector: (di) => di.provide(
|
||||||
|
InjectionKeys.useScriptDiagnosticsCollector,
|
||||||
|
useScriptDiagnosticsCollector,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
registerAll(Object.values(resolvers), api);
|
registerAll(Object.values(resolvers), api);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
import { defineComponent, computed } from 'vue';
|
import { defineComponent, computed } from 'vue';
|
||||||
import { injectKey } from '@/presentation/injectionSymbols';
|
import { injectKey } from '@/presentation/injectionSymbols';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { Dialog } from '@/presentation/common/Dialog';
|
|
||||||
import { CodeRunError } from '@/application/CodeRunner/CodeRunner';
|
|
||||||
import IconButton from './IconButton.vue';
|
import IconButton from './IconButton.vue';
|
||||||
|
import { createScriptErrorDialog } from './ScriptErrorDialog';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -24,6 +23,7 @@ export default defineComponent({
|
|||||||
const { os, isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
const { os, isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||||
const { codeRunner } = injectKey((keys) => keys.useCodeRunner);
|
const { codeRunner } = injectKey((keys) => keys.useCodeRunner);
|
||||||
const { dialog } = injectKey((keys) => keys.useDialog);
|
const { dialog } = injectKey((keys) => keys.useDialog);
|
||||||
|
const { scriptDiagnosticsCollector } = injectKey((keys) => keys.useScriptDiagnosticsCollector);
|
||||||
|
|
||||||
const canRun = computed<boolean>(() => getCanRunState(
|
const canRun = computed<boolean>(() => getCanRunState(
|
||||||
currentState.value.os,
|
currentState.value.os,
|
||||||
@@ -38,7 +38,12 @@ export default defineComponent({
|
|||||||
currentContext.state.collection.scripting.fileExtension,
|
currentContext.state.collection.scripting.fileExtension,
|
||||||
);
|
);
|
||||||
if (!success) {
|
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;
|
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||||
return isRunningAsDesktopApplication && isRunningOnSelectedOs;
|
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>
|
</script>
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue'
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ScriptFilename } from '@/application/CodeRunner/ScriptFilename';
|
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 IconButton from '../IconButton.vue';
|
||||||
|
import { createScriptErrorDialog } from '../ScriptErrorDialog';
|
||||||
import RunInstructions from './RunInstructions/RunInstructions.vue';
|
import RunInstructions from './RunInstructions/RunInstructions.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -34,6 +35,7 @@ export default defineComponent({
|
|||||||
const { currentState } = injectKey((keys) => keys.useCollectionState);
|
const { currentState } = injectKey((keys) => keys.useCollectionState);
|
||||||
const { isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
const { isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||||
const { dialog } = injectKey((keys) => keys.useDialog);
|
const { dialog } = injectKey((keys) => keys.useDialog);
|
||||||
|
const { scriptDiagnosticsCollector } = injectKey((keys) => keys.useScriptDiagnosticsCollector);
|
||||||
|
|
||||||
const areInstructionsVisible = ref(false);
|
const areInstructionsVisible = ref(false);
|
||||||
const filename = computed<string>(() => buildFilename(currentState.value.collection.scripting));
|
const filename = computed<string>(() => buildFilename(currentState.value.collection.scripting));
|
||||||
@@ -45,7 +47,12 @@ export default defineComponent({
|
|||||||
getType(currentState.value.collection.scripting.language),
|
getType(currentState.value.collection.scripting.language),
|
||||||
);
|
);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
showScriptSaveError(dialog, error);
|
dialog.showError(...(await createScriptErrorDialog({
|
||||||
|
errorContext: 'save',
|
||||||
|
errorType: error.type,
|
||||||
|
errorMessage: error.message,
|
||||||
|
isFileReadbackError: error.type === 'FileReadbackVerificationError',
|
||||||
|
}, scriptDiagnosticsCollector)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
areInstructionsVisible.value = true;
|
areInstructionsVisible.value = true;
|
||||||
@@ -77,60 +84,4 @@ function buildFilename(scripting: IScriptingDefinition) {
|
|||||||
}
|
}
|
||||||
return ScriptFilename;
|
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>
|
</script>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
These false positives are common for scripts that modify system settings.
|
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>
|
||||||
<p>
|
<p>
|
||||||
To handle false warnings in Microsoft Defender:
|
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 { Dialog } from '@/presentation/common/Dialog';
|
||||||
import { ElectronDialog } from '@/infrastructure/Dialog/Electron/ElectronDialog';
|
import { ElectronDialog } from '@/infrastructure/Dialog/Electron/ElectronDialog';
|
||||||
import { IpcChannel } from '@/presentation/electron/shared/IpcBridging/IpcChannel';
|
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 { registerIpcChannel } from '../shared/IpcBridging/IpcProxy';
|
||||||
import { ChannelDefinitionKey, IpcChannelDefinitions } from '../shared/IpcBridging/IpcChannelDefinitions';
|
import { ChannelDefinitionKey, IpcChannelDefinitions } from '../shared/IpcBridging/IpcChannelDefinitions';
|
||||||
|
|
||||||
export function registerAllIpcChannels(
|
export function registerAllIpcChannels(
|
||||||
|
registrar: IpcChannelRegistrar = registerIpcChannel,
|
||||||
createCodeRunner: CodeRunnerFactory = () => new ScriptFileCodeRunner(),
|
createCodeRunner: CodeRunnerFactory = () => new ScriptFileCodeRunner(),
|
||||||
createDialog: DialogFactory = () => new ElectronDialog(),
|
createDialog: DialogFactory = () => new ElectronDialog(),
|
||||||
registrar: IpcChannelRegistrar = registerIpcChannel,
|
createScriptDiagnosticsCollector
|
||||||
|
: ScriptDiagnosticsCollectorFactory = () => new ScriptEnvironmentDiagnosticsCollector(),
|
||||||
) {
|
) {
|
||||||
const ipcInstanceCreators: IpcChannelRegistrars = {
|
const ipcInstanceCreators: IpcChannelRegistrars = {
|
||||||
CodeRunner: () => createCodeRunner(),
|
CodeRunner: () => createCodeRunner(),
|
||||||
Dialog: () => createDialog(),
|
Dialog: () => createDialog(),
|
||||||
|
ScriptDiagnosticsCollector: () => createScriptDiagnosticsCollector(),
|
||||||
};
|
};
|
||||||
Object.entries(ipcInstanceCreators).forEach(([name, instanceFactory]) => {
|
Object.entries(ipcInstanceCreators).forEach(([name, instanceFactory]) => {
|
||||||
try {
|
try {
|
||||||
@@ -26,9 +31,11 @@ export function registerAllIpcChannels(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IpcChannelRegistrar = typeof registerIpcChannel;
|
||||||
|
|
||||||
export type CodeRunnerFactory = () => CodeRunner;
|
export type CodeRunnerFactory = () => CodeRunner;
|
||||||
export type DialogFactory = () => Dialog;
|
export type DialogFactory = () => Dialog;
|
||||||
export type IpcChannelRegistrar = typeof registerIpcChannel;
|
export type ScriptDiagnosticsCollectorFactory = () => ScriptDiagnosticsCollector;
|
||||||
|
|
||||||
type RegistrationChannel<T extends ChannelDefinitionKey> = (typeof IpcChannelDefinitions)[T];
|
type RegistrationChannel<T extends ChannelDefinitionKey> = (typeof IpcChannelDefinitions)[T];
|
||||||
type ExtractChannelServiceType<T> = T extends IpcChannel<infer U> ? U : never;
|
type ExtractChannelServiceType<T> = T extends IpcChannel<infer U> ? U : never;
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { IpcChannelDefinitions } from '../../shared/IpcBridging/IpcChannelDefini
|
|||||||
import { createSecureFacade } from './SecureFacadeCreator';
|
import { createSecureFacade } from './SecureFacadeCreator';
|
||||||
|
|
||||||
export function provideWindowVariables(
|
export function provideWindowVariables(
|
||||||
createLogger: LoggerFactory = () => createElectronLogger(),
|
|
||||||
convertToOs = convertPlatformToOs,
|
|
||||||
createApiFacade: ApiFacadeFactory = createSecureFacade,
|
createApiFacade: ApiFacadeFactory = createSecureFacade,
|
||||||
ipcConsumerCreator: IpcConsumerProxyCreator = createIpcConsumerProxy,
|
ipcConsumerCreator: IpcConsumerProxyCreator = createIpcConsumerProxy,
|
||||||
|
convertToOs = convertPlatformToOs,
|
||||||
|
createLogger: LoggerFactory = () => createElectronLogger(),
|
||||||
): WindowVariables {
|
): WindowVariables {
|
||||||
// Enforces mandatory variable availability at compile time
|
// Enforces mandatory variable availability at compile time
|
||||||
const variables: RequiredWindowVariables = {
|
const variables: RequiredWindowVariables = {
|
||||||
@@ -19,6 +19,9 @@ export function provideWindowVariables(
|
|||||||
os: convertToOs(process.platform),
|
os: convertToOs(process.platform),
|
||||||
codeRunner: ipcConsumerCreator(IpcChannelDefinitions.CodeRunner),
|
codeRunner: ipcConsumerCreator(IpcChannelDefinitions.CodeRunner),
|
||||||
dialog: ipcConsumerCreator(IpcChannelDefinitions.Dialog),
|
dialog: ipcConsumerCreator(IpcChannelDefinitions.Dialog),
|
||||||
|
scriptDiagnosticsCollector: ipcConsumerCreator(
|
||||||
|
IpcChannelDefinitions.ScriptDiagnosticsCollector,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
return variables;
|
return variables;
|
||||||
}
|
}
|
||||||
@@ -26,8 +29,8 @@ export function provideWindowVariables(
|
|||||||
type RequiredWindowVariables = PartiallyRequired<WindowVariables, 'os' /* | 'anotherOptionalKey'.. */>;
|
type RequiredWindowVariables = PartiallyRequired<WindowVariables, 'os' /* | 'anotherOptionalKey'.. */>;
|
||||||
type PartiallyRequired<T, K extends keyof T> = Required<Omit<T, K>> & Pick<T, K>;
|
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 ApiFacadeFactory = typeof createSecureFacade;
|
||||||
|
|
||||||
export type IpcConsumerProxyCreator = typeof createIpcConsumerProxy;
|
export type IpcConsumerProxyCreator = typeof createIpcConsumerProxy;
|
||||||
|
|
||||||
|
export type LoggerFactory = () => Logger;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { FunctionKeys } from '@/TypeHelpers';
|
import { FunctionKeys } from '@/TypeHelpers';
|
||||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||||
import { Dialog } from '@/presentation/common/Dialog';
|
import { Dialog } from '@/presentation/common/Dialog';
|
||||||
|
import { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||||
import { IpcChannel } from './IpcChannel';
|
import { IpcChannel } from './IpcChannel';
|
||||||
|
|
||||||
export const IpcChannelDefinitions = {
|
export const IpcChannelDefinitions = {
|
||||||
CodeRunner: defineElectronIpcChannel<CodeRunner>('code-run', ['runCode']),
|
CodeRunner: defineElectronIpcChannel<CodeRunner>('code-run', ['runCode']),
|
||||||
Dialog: defineElectronIpcChannel<Dialog>('dialogs', ['showError', 'saveFile']),
|
Dialog: defineElectronIpcChannel<Dialog>('dialogs', ['showError', 'saveFile']),
|
||||||
|
ScriptDiagnosticsCollector: defineElectronIpcChannel<ScriptDiagnosticsCollector>('script-diagnostics-collector', ['collectDiagnosticInformation']),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ChannelDefinitionKey = keyof typeof IpcChannelDefinitions;
|
export type ChannelDefinitionKey = keyof typeof IpcChannelDefinitions;
|
||||||
|
|||||||
@@ -51,8 +51,11 @@
|
|||||||
</style>
|
</style>
|
||||||
<div id="javascriptDisabled">
|
<div id="javascriptDisabled">
|
||||||
<h1>Problem loading page</h1>
|
<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
|
<p>
|
||||||
as 100% of the website is open source.</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>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<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 { useLogger } from '@/presentation/components/Shared/Hooks/Log/UseLogger';
|
||||||
import type { useCodeRunner } from './components/Shared/Hooks/UseCodeRunner';
|
import type { useCodeRunner } from './components/Shared/Hooks/UseCodeRunner';
|
||||||
import type { useDialog } from './components/Shared/Hooks/Dialog/UseDialog';
|
import type { useDialog } from './components/Shared/Hooks/Dialog/UseDialog';
|
||||||
|
import type { useScriptDiagnosticsCollector } from './components/Shared/Hooks/UseScriptDiagnosticsCollector';
|
||||||
|
|
||||||
export const InjectionKeys = {
|
export const InjectionKeys = {
|
||||||
useCollectionState: defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState'),
|
useCollectionState: defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState'),
|
||||||
@@ -21,6 +22,7 @@ export const InjectionKeys = {
|
|||||||
useLogger: defineTransientKey<ReturnType<typeof useLogger>>('useLogger'),
|
useLogger: defineTransientKey<ReturnType<typeof useLogger>>('useLogger'),
|
||||||
useCodeRunner: defineTransientKey<ReturnType<typeof useCodeRunner>>('useCodeRunner'),
|
useCodeRunner: defineTransientKey<ReturnType<typeof useCodeRunner>>('useCodeRunner'),
|
||||||
useDialog: defineTransientKey<ReturnType<typeof useDialog>>('useDialog'),
|
useDialog: defineTransientKey<ReturnType<typeof useDialog>>('useDialog'),
|
||||||
|
useScriptDiagnosticsCollector: defineTransientKey<ReturnType<typeof useScriptDiagnosticsCollector>>('useScriptDiagnostics'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface InjectionKeyWithLifetime<T> {
|
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 { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
BrowserRuntimeEnvironmentFactory, GlobalPropertiesAccessor, NodeRuntimeEnvironmentFactory,
|
BrowserRuntimeEnvironmentFactory, NodeRuntimeEnvironmentFactory,
|
||||||
determineAndCreateRuntimeEnvironment,
|
determineAndCreateRuntimeEnvironment,
|
||||||
} from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
} from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||||
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
|
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('RuntimeEnvironmentFactory', () => {
|
||||||
describe('determineAndCreateRuntimeEnvironment', () => {
|
describe('determineAndCreateRuntimeEnvironment', () => {
|
||||||
describe('Node environment creation', () => {
|
describe('Node environment creation', () => {
|
||||||
it('selects Node environment if Electron main process detected', () => {
|
it('creates Node environment in Electron main process', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const processStub = createProcessStub({
|
|
||||||
versions: {
|
|
||||||
electron: '28.1.3',
|
|
||||||
} as NodeJS.ProcessVersions,
|
|
||||||
});
|
|
||||||
const expectedEnvironment = new RuntimeEnvironmentStub();
|
const expectedEnvironment = new RuntimeEnvironmentStub();
|
||||||
|
const mainProcessDetector = new ElectronEnvironmentDetectorStub()
|
||||||
|
.withElectronEnvironment('main');
|
||||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||||
.withGlobalProcess(processStub)
|
.withElectronEnvironmentDetector(mainProcessDetector)
|
||||||
.withNodeEnvironmentFactory(() => expectedEnvironment);
|
.withNodeEnvironmentFactory(() => expectedEnvironment);
|
||||||
// act
|
// act
|
||||||
const actualEnvironment = context.buildEnvironment();
|
const actualEnvironment = context.buildEnvironment();
|
||||||
// assert
|
// assert
|
||||||
expect(actualEnvironment).to.equal(expectedEnvironment);
|
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', () => {
|
describe('browser environment creation', () => {
|
||||||
it('selects browser environment if Electron main process not detected', () => {
|
it('creates browser environment in Electron renderer process', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedEnvironment = new RuntimeEnvironmentStub();
|
const expectedEnvironment = new RuntimeEnvironmentStub();
|
||||||
const undefinedProcess: GlobalProcess = undefined;
|
const rendererProcessDetector = new ElectronEnvironmentDetectorStub()
|
||||||
|
.withElectronEnvironment('renderer');
|
||||||
const windowStub = createWindowStub();
|
const windowStub = createWindowStub();
|
||||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||||
.withGlobalProcess(undefinedProcess)
|
.withElectronEnvironmentDetector(rendererProcessDetector)
|
||||||
.withGlobalWindow(windowStub)
|
.withGlobalWindow(windowStub)
|
||||||
.withBrowserEnvironmentFactory(() => expectedEnvironment);
|
.withBrowserEnvironmentFactory(() => expectedEnvironment);
|
||||||
// act
|
// act
|
||||||
@@ -60,21 +40,37 @@ describe('RuntimeEnvironmentFactory', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(actualEnvironment).to.equal(expectedEnvironment);
|
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
|
// arrange
|
||||||
const expectedWindow = createWindowStub({
|
const expectedWindow = createWindowStub({
|
||||||
isRunningAsDesktopApplication: undefined,
|
isRunningAsDesktopApplication: undefined,
|
||||||
});
|
});
|
||||||
let actualWindow: GlobalWindow;
|
let actualWindow: Window | undefined;
|
||||||
const browserEnvironmentFactoryMock
|
const browserEnvironmentFactoryMock
|
||||||
: BrowserRuntimeEnvironmentFactory = (providedWindow) => {
|
: BrowserRuntimeEnvironmentFactory = (providedWindow) => {
|
||||||
actualWindow = providedWindow;
|
actualWindow = providedWindow;
|
||||||
return new RuntimeEnvironmentStub();
|
return new RuntimeEnvironmentStub();
|
||||||
};
|
};
|
||||||
const undefinedProcess: GlobalProcess = undefined;
|
const nonElectronDetector = new ElectronEnvironmentDetectorStub()
|
||||||
|
.withNonElectronEnvironment();
|
||||||
const context = new RuntimeEnvironmentFactoryTestSetup()
|
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||||
.withGlobalWindow(expectedWindow)
|
.withGlobalWindow(expectedWindow)
|
||||||
.withGlobalProcess(undefinedProcess)
|
.withElectronEnvironmentDetector(nonElectronDetector)
|
||||||
.withBrowserEnvironmentFactory(browserEnvironmentFactoryMock);
|
.withBrowserEnvironmentFactory(browserEnvironmentFactoryMock);
|
||||||
// act
|
// act
|
||||||
context.buildEnvironment();
|
context.buildEnvironment();
|
||||||
@@ -82,14 +78,15 @@ describe('RuntimeEnvironmentFactory', () => {
|
|||||||
expect(actualWindow).to.equal(expectedWindow);
|
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
|
// 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 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()
|
const context = new RuntimeEnvironmentFactoryTestSetup()
|
||||||
.withGlobalProcess(undefinedProcess)
|
.withElectronEnvironmentDetector(nonElectronDetector)
|
||||||
.withGlobalWindow(undefinedWindow);
|
.withGlobalWindow(nullWindow);
|
||||||
// act
|
// act
|
||||||
const act = () => context.buildEnvironment();
|
const act = () => context.buildEnvironment();
|
||||||
// assert
|
// assert
|
||||||
@@ -104,16 +101,11 @@ function createWindowStub(partialWindowProperties?: Partial<Window>): Window {
|
|||||||
} as Window;
|
} as Window;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createProcessStub(partialProcessProperties?: Partial<NodeJS.Process>): NodeJS.Process {
|
|
||||||
return {
|
|
||||||
...partialProcessProperties,
|
|
||||||
} as NodeJS.Process;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RuntimeEnvironmentFactoryTestSetup {
|
export class RuntimeEnvironmentFactoryTestSetup {
|
||||||
private globalWindow: GlobalWindow = createWindowStub();
|
private globalWindow: Window | undefined | null = createWindowStub();
|
||||||
|
|
||||||
private globalProcess: GlobalProcess = createProcessStub();
|
private electronEnvironmentDetector
|
||||||
|
: ElectronEnvironmentDetector = new ElectronEnvironmentDetectorStub();
|
||||||
|
|
||||||
private browserEnvironmentFactory
|
private browserEnvironmentFactory
|
||||||
: BrowserRuntimeEnvironmentFactory = () => new RuntimeEnvironmentStub();
|
: BrowserRuntimeEnvironmentFactory = () => new RuntimeEnvironmentStub();
|
||||||
@@ -121,13 +113,13 @@ export class RuntimeEnvironmentFactoryTestSetup {
|
|||||||
private nodeEnvironmentFactory
|
private nodeEnvironmentFactory
|
||||||
: NodeRuntimeEnvironmentFactory = () => new RuntimeEnvironmentStub();
|
: NodeRuntimeEnvironmentFactory = () => new RuntimeEnvironmentStub();
|
||||||
|
|
||||||
public withGlobalWindow(globalWindow: GlobalWindow): this {
|
public withGlobalWindow(globalWindow: Window | undefined | null): this {
|
||||||
this.globalWindow = globalWindow;
|
this.globalWindow = globalWindow;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withGlobalProcess(globalProcess: GlobalProcess): this {
|
public withElectronEnvironmentDetector(detector: ElectronEnvironmentDetector): this {
|
||||||
this.globalProcess = globalProcess;
|
this.electronEnvironmentDetector = detector;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,16 +139,10 @@ export class RuntimeEnvironmentFactoryTestSetup {
|
|||||||
|
|
||||||
public buildEnvironment(): ReturnType<typeof determineAndCreateRuntimeEnvironment> {
|
public buildEnvironment(): ReturnType<typeof determineAndCreateRuntimeEnvironment> {
|
||||||
return determineAndCreateRuntimeEnvironment(
|
return determineAndCreateRuntimeEnvironment(
|
||||||
{
|
this.globalWindow,
|
||||||
window: this.globalWindow,
|
this.electronEnvironmentDetector,
|
||||||
process: this.globalProcess,
|
|
||||||
},
|
|
||||||
this.browserEnvironmentFactory,
|
this.browserEnvironmentFactory,
|
||||||
this.nodeEnvironmentFactory,
|
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 { validateWindowVariables } from '@/infrastructure/WindowVariables/WindowVariablesValidator';
|
||||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
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 { WindowVariablesStub } from '@tests/unit/shared/Stubs/WindowVariablesStub';
|
||||||
import { CodeRunnerStub } from '@tests/unit/shared/Stubs/CodeRunnerStub';
|
import { CodeRunnerStub } from '@tests/unit/shared/Stubs/CodeRunnerStub';
|
||||||
import { PropertyKeys } from '@/TypeHelpers';
|
import { PropertyKeys } from '@/TypeHelpers';
|
||||||
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
||||||
import { DialogStub } from '@tests/unit/shared/Stubs/DialogStub';
|
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('WindowVariablesValidator', () => {
|
||||||
describe('validateWindowVariables', () => {
|
describe('validateWindowVariables', () => {
|
||||||
describe('validates window type', () => {
|
it('throws an error with a description of all invalid properties', () => {
|
||||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
// arrange
|
||||||
// arrange
|
const invalidOs = 'invalid' as unknown as OperatingSystem;
|
||||||
const expectedError = 'window is not an object';
|
const invalidIsRunningAsDesktopApplication = 'not a boolean' as never;
|
||||||
const window: Partial<WindowVariables> = invalidObjectValue as never;
|
const expectedError = getExpectedError(
|
||||||
// act
|
{
|
||||||
const act = () => validateWindowVariables(window);
|
name: 'os',
|
||||||
// assert
|
value: invalidOs,
|
||||||
expect(act).to.throw(expectedError);
|
},
|
||||||
});
|
{
|
||||||
});
|
name: 'isRunningAsDesktopApplication',
|
||||||
|
value: invalidIsRunningAsDesktopApplication,
|
||||||
describe('property validations', () => {
|
},
|
||||||
it('throws an error with a description of all invalid properties', () => {
|
);
|
||||||
// arrange
|
const input = new WindowVariablesStub()
|
||||||
const invalidOs = 'invalid' as unknown as OperatingSystem;
|
.withOs(invalidOs)
|
||||||
const invalidIsRunningAsDesktopApplication = 'not a boolean' as never;
|
.withIsRunningAsDesktopApplication(invalidIsRunningAsDesktopApplication);
|
||||||
const expectedError = getExpectedError(
|
const context = new ValidateWindowVariablesTestSetup()
|
||||||
{
|
.withWindowVariables(input);
|
||||||
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();
|
|
||||||
// act
|
// act
|
||||||
const act = () => validateWindowVariables(input);
|
const act = () => context.validateWindowVariables();
|
||||||
// assert
|
// 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) {
|
class ValidateWindowVariablesTestSetup {
|
||||||
describe('validates object type on desktop', () => {
|
private electronDetector: ElectronEnvironmentDetector = new ElectronEnvironmentDetectorStub()
|
||||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
.withElectronEnvironment('renderer');
|
||||||
// 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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function itEachInvalidObjectValue<T>(runner: (invalidObjectValue: T) => void) {
|
private windowVariables: WindowVariables = new WindowVariablesStub();
|
||||||
const testCases: Array<{
|
|
||||||
readonly name: string;
|
public withWindowVariables(windowVariables: WindowVariables): this {
|
||||||
readonly value: T;
|
this.windowVariables = windowVariables;
|
||||||
}> = [
|
return this;
|
||||||
{
|
}
|
||||||
name: 'given string',
|
|
||||||
value: 'invalid object' as unknown as T,
|
public withElectronDetector(electronDetector: ElectronEnvironmentDetector): this {
|
||||||
},
|
this.electronDetector = electronDetector;
|
||||||
{
|
return this;
|
||||||
name: 'given array of objects',
|
}
|
||||||
value: [{}, {}] as unknown as T,
|
|
||||||
},
|
public validateWindowVariables() {
|
||||||
...getAbsentObjectTestCases().map((testCase) => ({
|
return validateWindowVariables(
|
||||||
name: `given absent: ${testCase.valueName}`,
|
this.windowVariables,
|
||||||
value: testCase.absentValue as unknown as T,
|
this.electronDetector,
|
||||||
})),
|
);
|
||||||
];
|
}
|
||||||
testCases.forEach((testCase) => {
|
|
||||||
it(testCase.name, () => {
|
|
||||||
runner(testCase.value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExpectedError(...unexpectedObjects: Array<{
|
function getExpectedError(...unexpectedObjects: Array<{
|
||||||
readonly name: keyof WindowVariables;
|
readonly name: keyof WindowVariables;
|
||||||
readonly object: unknown;
|
readonly value: unknown;
|
||||||
}>) {
|
}>) {
|
||||||
const errors = unexpectedObjects
|
const errors = unexpectedObjects
|
||||||
.map(({ name, object }) => `Unexpected ${name} (${typeof object})`);
|
.map(({ name, value: object }) => `Unexpected ${name} (${typeof object})`);
|
||||||
return errors.join('\n');
|
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(),
|
useLogger: createTransientTests(),
|
||||||
useCodeRunner: createTransientTests(),
|
useCodeRunner: createTransientTests(),
|
||||||
useDialog: createTransientTests(),
|
useDialog: createTransientTests(),
|
||||||
|
useScriptDiagnosticsCollector: createTransientTests(),
|
||||||
};
|
};
|
||||||
Object.entries(testCases).forEach(([key, runTests]) => {
|
Object.entries(testCases).forEach(([key, runTests]) => {
|
||||||
const registeredKey = InjectionKeys[key].key;
|
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 { CodeRunnerStub } from '@tests/unit/shared/Stubs/CodeRunnerStub';
|
||||||
import { ChannelDefinitionKey, IpcChannelDefinitions } from '@/presentation/electron/shared/IpcBridging/IpcChannelDefinitions';
|
import { ChannelDefinitionKey, IpcChannelDefinitions } from '@/presentation/electron/shared/IpcBridging/IpcChannelDefinitions';
|
||||||
import {
|
import {
|
||||||
CodeRunnerFactory, DialogFactory, IpcChannelRegistrar, registerAllIpcChannels,
|
CodeRunnerFactory, DialogFactory, IpcChannelRegistrar,
|
||||||
|
ScriptDiagnosticsCollectorFactory, registerAllIpcChannels,
|
||||||
} from '@/presentation/electron/main/IpcRegistration';
|
} from '@/presentation/electron/main/IpcRegistration';
|
||||||
import { IpcChannel } from '@/presentation/electron/shared/IpcBridging/IpcChannel';
|
import { IpcChannel } from '@/presentation/electron/shared/IpcBridging/IpcChannel';
|
||||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||||
import { DialogStub } from '@tests/unit/shared/Stubs/DialogStub';
|
import { DialogStub } from '@tests/unit/shared/Stubs/DialogStub';
|
||||||
|
import { ScriptDiagnosticsCollectorStub } from '../../../shared/Stubs/ScriptDiagnosticsCollectorStub';
|
||||||
|
|
||||||
describe('IpcRegistration', () => {
|
describe('IpcRegistration', () => {
|
||||||
describe('registerAllIpcChannels', () => {
|
describe('registerAllIpcChannels', () => {
|
||||||
@@ -44,6 +46,13 @@ describe('IpcRegistration', () => {
|
|||||||
expectedInstance,
|
expectedInstance,
|
||||||
};
|
};
|
||||||
})(),
|
})(),
|
||||||
|
ScriptDiagnosticsCollector: (() => {
|
||||||
|
const expectedInstance = new ScriptDiagnosticsCollectorStub();
|
||||||
|
return {
|
||||||
|
buildContext: (c) => c.witScriptDiagnosticsCollectorFactory(() => expectedInstance),
|
||||||
|
expectedInstance,
|
||||||
|
};
|
||||||
|
})(),
|
||||||
};
|
};
|
||||||
Object.entries(testScenarios).forEach(([
|
Object.entries(testScenarios).forEach(([
|
||||||
key, { buildContext, expectedInstance },
|
key, { buildContext, expectedInstance },
|
||||||
@@ -79,11 +88,14 @@ describe('IpcRegistration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
class IpcRegistrationTestSetup {
|
class IpcRegistrationTestSetup {
|
||||||
|
private registrar: IpcChannelRegistrar = () => { /* NOOP */ };
|
||||||
|
|
||||||
private codeRunnerFactory: CodeRunnerFactory = () => new CodeRunnerStub();
|
private codeRunnerFactory: CodeRunnerFactory = () => new CodeRunnerStub();
|
||||||
|
|
||||||
private dialogFactory: DialogFactory = () => new DialogStub();
|
private dialogFactory: DialogFactory = () => new DialogStub();
|
||||||
|
|
||||||
private registrar: IpcChannelRegistrar = () => { /* NOOP */ };
|
private scriptDiagnosticsCollectorFactory
|
||||||
|
: ScriptDiagnosticsCollectorFactory = () => new ScriptDiagnosticsCollectorStub();
|
||||||
|
|
||||||
public withRegistrar(registrar: IpcChannelRegistrar): this {
|
public withRegistrar(registrar: IpcChannelRegistrar): this {
|
||||||
this.registrar = registrar;
|
this.registrar = registrar;
|
||||||
@@ -100,11 +112,19 @@ class IpcRegistrationTestSetup {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public witScriptDiagnosticsCollectorFactory(
|
||||||
|
scriptDiagnosticsCollectorFactory: ScriptDiagnosticsCollectorFactory,
|
||||||
|
): this {
|
||||||
|
this.scriptDiagnosticsCollectorFactory = scriptDiagnosticsCollectorFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public run() {
|
public run() {
|
||||||
registerAllIpcChannels(
|
registerAllIpcChannels(
|
||||||
|
this.registrar,
|
||||||
this.codeRunnerFactory,
|
this.codeRunnerFactory,
|
||||||
this.dialogFactory,
|
this.dialogFactory,
|
||||||
this.registrar,
|
this.scriptDiagnosticsCollectorFactory,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ describe('RendererApiProvider', () => {
|
|||||||
setupContext(context: RendererApiProviderTestContext): RendererApiProviderTestContext;
|
setupContext(context: RendererApiProviderTestContext): RendererApiProviderTestContext;
|
||||||
readonly expectedValue: unknown;
|
readonly expectedValue: unknown;
|
||||||
}
|
}
|
||||||
const testScenarios: Record<PropertyKeys<Required<WindowVariables>>, WindowVariableTestCase> = {
|
const testScenarios: Record<
|
||||||
|
PropertyKeys<Required<WindowVariables>>,
|
||||||
|
WindowVariableTestCase> = {
|
||||||
isRunningAsDesktopApplication: {
|
isRunningAsDesktopApplication: {
|
||||||
description: 'returns true',
|
description: 'returns true',
|
||||||
setupContext: (context) => context,
|
setupContext: (context) => context,
|
||||||
@@ -32,9 +34,12 @@ describe('RendererApiProvider', () => {
|
|||||||
})(),
|
})(),
|
||||||
log: expectFacade({
|
log: expectFacade({
|
||||||
instance: new LoggerStub(),
|
instance: new LoggerStub(),
|
||||||
setupContext: (c, logger) => c.withLogger(logger),
|
setupContext: (c, instance) => c.withLogger(instance),
|
||||||
}),
|
}),
|
||||||
dialog: expectIpcConsumer(IpcChannelDefinitions.Dialog),
|
dialog: expectIpcConsumer(IpcChannelDefinitions.Dialog),
|
||||||
|
scriptDiagnosticsCollector: expectIpcConsumer(
|
||||||
|
IpcChannelDefinitions.ScriptDiagnosticsCollector,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
Object.entries(testScenarios).forEach((
|
Object.entries(testScenarios).forEach((
|
||||||
[property, { description, setupContext, expectedValue }],
|
[property, { description, setupContext, expectedValue }],
|
||||||
@@ -109,10 +114,10 @@ class RendererApiProviderTestContext {
|
|||||||
|
|
||||||
public provideWindowVariables() {
|
public provideWindowVariables() {
|
||||||
return provideWindowVariables(
|
return provideWindowVariables(
|
||||||
() => this.log,
|
|
||||||
() => this.os,
|
|
||||||
this.apiFacadeCreator,
|
this.apiFacadeCreator,
|
||||||
this.ipcConsumerCreator,
|
this.ipcConsumerCreator,
|
||||||
|
() => this.os,
|
||||||
|
() => this.log,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ describe('IpcChannelDefinitions', () => {
|
|||||||
expectedNamespace: 'dialogs',
|
expectedNamespace: 'dialogs',
|
||||||
expectedAccessibleMembers: ['saveFile'],
|
expectedAccessibleMembers: ['saveFile'],
|
||||||
},
|
},
|
||||||
|
ScriptDiagnosticsCollector: {
|
||||||
|
expectedNamespace: 'script-diagnostics-collector',
|
||||||
|
expectedAccessibleMembers: ['collectDiagnosticInformation'],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
Object.entries(testScenarios).forEach((
|
Object.entries(testScenarios).forEach((
|
||||||
[definitionKey, { expectedNamespace, expectedAccessibleMembers }],
|
[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 { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||||
import { Dialog } from '@/presentation/common/Dialog';
|
import { Dialog } from '@/presentation/common/Dialog';
|
||||||
|
import { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||||
import { LoggerStub } from './LoggerStub';
|
import { LoggerStub } from './LoggerStub';
|
||||||
import { CodeRunnerStub } from './CodeRunnerStub';
|
import { CodeRunnerStub } from './CodeRunnerStub';
|
||||||
import { DialogStub } from './DialogStub';
|
import { DialogStub } from './DialogStub';
|
||||||
|
import { ScriptDiagnosticsCollectorStub } from './ScriptDiagnosticsCollectorStub';
|
||||||
|
|
||||||
export class WindowVariablesStub implements WindowVariables {
|
export class WindowVariablesStub implements WindowVariables {
|
||||||
public codeRunner?: CodeRunner = new CodeRunnerStub();
|
public codeRunner?: CodeRunner = new CodeRunnerStub();
|
||||||
@@ -18,6 +20,16 @@ export class WindowVariablesStub implements WindowVariables {
|
|||||||
|
|
||||||
public dialog?: Dialog = new DialogStub();
|
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 {
|
public withLog(log: Logger): this {
|
||||||
this.log = log;
|
this.log = log;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
Reference in New Issue
Block a user