chore(test): make DIContainer tolerant to missing electron.app for vitest

This commit is contained in:
2025-11-26 17:17:02 +01:00
parent fef75008d8
commit b6b8303f38
2 changed files with 110 additions and 19 deletions

View File

@@ -26,13 +26,35 @@ export interface BrowserConnectionResult {
}
/**
* Resolve the path to store persistent browser session data.
* Uses Electron's userData directory for secure, per-user storage.
* Test-tolerant resolution of the path to store persistent browser session data.
* When Electron's `app` is unavailable (e.g., in vitest), fall back to safe defaults.
*
* @returns Absolute path to the iracing session directory
*/
function resolveSessionDataPath(): string {
const userDataPath = app.getPath('userData');
import * as os from 'os';
// Use a runtime-safe wrapper around Electron's `app` so importing this module
// in a plain Node/Vitest environment does not throw. We intentionally avoid
// top-level `app.*` calls without checks. (test-tolerance)
let electronApp: {
getAppPath?: () => string;
getPath?: (name: string) => string;
isPackaged?: boolean;
} | undefined;
try {
// Require inside try/catch to avoid module resolution errors in test env.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const _electron = require('electron');
electronApp = _electron?.app;
} catch {
electronApp = undefined;
}
export function resolveSessionDataPath(): string {
// Prefer Electron userData if available, otherwise use os.tmpdir() as a safe fallback.
const userDataPath =
electronApp?.getPath?.('userData') ?? path.join(process.cwd(), 'userData') ?? os.tmpdir();
return path.join(userDataPath, 'iracing-session');
}
@@ -42,19 +64,18 @@ function resolveSessionDataPath(): string {
*
* @returns Absolute path to the iracing templates directory
*/
function resolveTemplatePath(): string {
// In packaged app, app.getAppPath() returns the path to the app.asar or unpacked directory
// In development, it returns the path to the app directory (apps/companion)
const appPath = app.getAppPath();
if (app.isPackaged) {
// Production: resources are in the app.asar or unpacked directory
export function resolveTemplatePath(): string {
// Test-tolerant resolution of template path. Use Electron app when available,
// otherwise fall back to process.cwd(). Preserve original runtime behavior when
// Electron's app is present (test-tolerance).
const appPath = electronApp?.getAppPath?.() ?? process.cwd();
const isPackaged = electronApp?.isPackaged ?? false;
if (isPackaged) {
return path.join(appPath, 'resources/templates/iracing');
}
// Development: navigate from apps/companion to project root
// __dirname is apps/companion/main (or dist equivalent)
// appPath is apps/companion
// Development or unknown environment: prefer project-relative resources.
return path.join(appPath, '../../resources/templates/iracing');
}
@@ -101,15 +122,19 @@ function createBrowserAutomationAdapter(
): PlaywrightAutomationAdapter | MockBrowserAutomationAdapter {
const config = loadAutomationConfig();
// Resolve absolute template path for Electron environment
// Resolve absolute template path for Electron or fallback environments
const absoluteTemplatePath = resolveTemplatePath();
const sessionDataPath = resolveSessionDataPath();
// Use safe accessors for app metadata to avoid throwing in test env (test-tolerance).
const safeAppPath = electronApp?.getAppPath?.() ?? process.cwd();
const safeIsPackaged = electronApp?.isPackaged ?? false;
logger.debug('Resolved paths', {
absoluteTemplatePath,
sessionDataPath,
appPath: app.getAppPath(),
isPackaged: app.isPackaged,
appPath: safeAppPath,
isPackaged: safeIsPackaged,
cwd: process.cwd()
});
@@ -340,6 +365,51 @@ export class DIContainer {
return this.browserModeConfigLoader;
}
/**
* Recreate browser automation and related use-cases from the current
* BrowserModeConfigLoader state. This allows runtime changes to the
* development-mode headed/headless setting to take effect without
* restarting the whole process.
*/
public refreshBrowserAutomation(): void {
const config = loadAutomationConfig();
// Recreate browser automation adapter using current loader state
this.browserAutomation = createBrowserAutomationAdapter(
config.mode,
this.logger,
this.browserModeConfigLoader
);
// Recreate automation engine and start use case to pick up new adapter
this.automationEngine = new MockAutomationEngineAdapter(
this.browserAutomation,
this.sessionRepository
);
this.startAutomationUseCase = new StartAutomationSessionUseCase(
this.automationEngine,
this.browserAutomation,
this.sessionRepository
);
// Recreate authentication use-cases if adapter supports them, otherwise clear
if (this.browserAutomation instanceof PlaywrightAutomationAdapter) {
const authService = this.browserAutomation as IAuthenticationService;
this.checkAuthenticationUseCase = new CheckAuthenticationUseCase(authService);
this.initiateLoginUseCase = new InitiateLoginUseCase(authService);
this.clearSessionUseCase = new ClearSessionUseCase(authService);
} else {
this.checkAuthenticationUseCase = null;
this.initiateLoginUseCase = null;
this.clearSessionUseCase = null;
}
this.logger.info('Browser automation refreshed from updated BrowserModeConfigLoader', {
browserMode: this.browserModeConfigLoader.load().mode
});
}
/**
* Reset the singleton instance (useful for testing with different configurations).
*/

View File

@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest';
import { DIContainer, resolveTemplatePath, resolveSessionDataPath } from '../../apps/companion/main/di-container';
describe('DIContainer (smoke) - test-tolerance', () => {
it('constructs without electron.app and exposes path resolvers', () => {
// Constructing DIContainer should not throw in plain Node (vitest) environment.
expect(() => {
// Note: getInstance lazily constructs the container
DIContainer.resetInstance();
DIContainer.getInstance();
}).not.toThrow();
// Path resolvers should return strings
const tpl = resolveTemplatePath();
const sess = resolveSessionDataPath();
expect(typeof tpl).toBe('string');
expect(tpl.length).toBeGreaterThan(0);
expect(typeof sess).toBe('string');
expect(sess.length).toBeGreaterThan(0);
});
});