chore(test): make DIContainer tolerant to missing electron.app for vitest
This commit is contained in:
@@ -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).
|
||||
*/
|
||||
|
||||
21
tests/smoke/di-container.test.ts
Normal file
21
tests/smoke/di-container.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user