Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 38 additions & 21 deletions packages/electron-service/src/classMock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Mock, fn as vitestFn } from '@wdio/native-spy';
import type { AbstractFn, ElectronClassMock, ElectronFunctionMock, ExecuteOpts } from '@wdio/native-types';
import { createLogger } from '@wdio/native-utils';
import { buildMockMethods } from './mockFactory.js';
import { buildMockMethods, type MockApplyData, type MockReadAccessor } from './mockFactory.js';
import mockStore from './mockStore.js';

const log = createLogger('electron-service', 'mock');
Expand Down Expand Up @@ -52,6 +52,24 @@ async function createPrototypeMock(
browserToUse,
});

const applyCalls = (data: MockApplyData) => {
const calls = data.calls ?? [];
const results = data.results ?? [];
const existingCount = originalMock.calls.length;
if (existingCount < calls.length) {
for (let i = existingCount; i < calls.length; i++) {
(originalMock.calls as unknown[][]).push(calls[i]);
(originalMock.results as { type: string; value: unknown }[]).push(
results[i] ?? { type: 'return', value: undefined },
);
(originalMock.invocationCallOrder as number[]).push(originalMock.invocationCallOrder.length);
}
}
};

mock.__accessor = { kind: 'prototype', className, methodName } satisfies MockReadAccessor;
mock.__applyCalls = applyCalls;

mock.update = async () => {
const syncData = (await browserToUse.electron.execute<
{ calls: unknown[][]; results: { type: string; value: unknown }[] },
Expand All @@ -71,16 +89,7 @@ async function createPrototypeMock(
{ internal: true },
)) ?? { calls: [], results: [] };

const existingCount = originalMock.calls.length;
if (existingCount < syncData.calls.length) {
for (let i = existingCount; i < syncData.calls.length; i++) {
(originalMock.calls as unknown[][]).push(syncData.calls[i]);
(originalMock.results as { type: string; value: unknown }[]).push(
syncData.results[i] ?? { type: 'return', value: undefined },
);
(originalMock.invocationCallOrder as number[]).push(originalMock.invocationCallOrder.length);
}
}
applyCalls(syncData);
return mock;
};

Expand Down Expand Up @@ -190,6 +199,23 @@ export async function createClassMock(
{ internal: true },
);

const applyConstructorCalls = (data: MockApplyData) => {
const calls = data.calls ?? [];
const existingCount = constructorOriginalMock.calls.length;
if (existingCount < calls.length) {
// Load-bearing for diagnosing constructor-mock sync races
log.debug(
`[${className}.__constructor] applying ${calls.length - existingCount} new constructor calls (inner=${calls.length}, outer=${existingCount})`,
);
for (let i = existingCount; i < calls.length; i++) {
(constructorMock as unknown as (...args: unknown[]) => unknown).apply(constructorMock, calls[i] as unknown[]);
}
}
};

constructorMock.__accessor = { kind: 'constructor', className } satisfies MockReadAccessor;
constructorMock.__applyCalls = applyConstructorCalls;

constructorMock.update = async () => {
const calls =
(await browserToUse.electron.execute<unknown[][], [string, ExecuteOpts]>(
Expand All @@ -203,16 +229,7 @@ export async function createClassMock(
{ internal: true },
)) ?? [];

const existingCount = constructorOriginalMock.calls.length;
if (existingCount < calls.length) {
// Load-bearing for diagnosing constructor-mock sync races
log.debug(
`[${className}.__constructor] mock.update: applying ${calls.length - existingCount} new constructor calls (inner=${calls.length}, outer=${existingCount})`,
);
for (let i = existingCount; i < calls.length; i++) {
(constructorMock as unknown as (...args: unknown[]) => unknown).apply(constructorMock, calls[i] as unknown[]);
}
}
applyConstructorCalls({ calls });
return constructorMock;
};

Expand Down
63 changes: 42 additions & 21 deletions packages/electron-service/src/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
ExecuteOpts,
} from '@wdio/native-types';
import { createLogger } from '@wdio/native-utils';
import { buildMockMethods } from './mockFactory.js';
import { buildMockMethods, type MockApplyData, type MockReadAccessor } from './mockFactory.js';

const log = createLogger('electron-service', 'mock');
const browserInterceptor = createIpcInterceptor('electron');
Expand Down Expand Up @@ -109,6 +109,27 @@ export async function createMock(
{ internal: true },
);

// Local diff-and-apply used by both the batched scheduler and mock.update().
// No I/O — safe to call synchronously with already-fetched call data.
const applyCalls = (data: MockApplyData) => {
const calls = data.calls ?? [];
// Load-bearing for diagnosing mock-sync races: shows inner/outer call counts
// at the moment new calls are applied so empty-mock.calls assertions can be traced.
if (originalMock.calls.length < calls.length) {
log.debug(
`[${apiName}.${funcName}] applying ${calls.length - originalMock.calls.length} new calls (inner=${calls.length}, outer=${originalMock.calls.length})`,
);
calls.forEach((call: unknown[], index: number) => {
if (!originalMock.calls[index]) {
mock?.apply(mock, call);
}
});
}
};

mock.__accessor = { kind: 'api', apiName, funcName } satisfies MockReadAccessor;
mock.__applyCalls = applyCalls;

mock.update = async () => {
const calls =
(await browserToUse.electron.execute<unknown[][], [string, string, ExecuteOpts]>(
Expand All @@ -123,18 +144,7 @@ export async function createMock(
{ internal: true },
)) ?? [];

// Load-bearing for diagnosing mock-sync races: shows inner/outer call counts
// at the moment update() runs so empty-mock.calls assertions can be traced.
if (originalMock.calls.length < calls.length) {
log.debug(
`[${apiName}.${funcName}] mock.update: applying ${calls.length - originalMock.calls.length} new calls (inner=${calls.length}, outer=${originalMock.calls.length})`,
);
calls.forEach((call: unknown[], index: number) => {
if (!originalMock.calls[index]) {
mock?.apply(mock, call);
}
});
}
applyCalls({ calls });

return mock;
};
Expand Down Expand Up @@ -169,6 +179,8 @@ export async function createMock(
wrapperMock.update = mock.update.bind(mock);

wrapperMock.__isElectronMock = true;
wrapperMock.__accessor = mock.__accessor;
wrapperMock.__applyCalls = mock.__applyCalls.bind(mock);

return wrapperMock;
}
Expand Down Expand Up @@ -205,22 +217,31 @@ export async function createElectronBrowserModeMock(

await runInterceptorScript<void>(browser, browserInterceptor.buildRegistrationScript(channel));

mock.update = async () => {
const raw = await runInterceptorScript<unknown>(browser, browserInterceptor.buildCallDataReadScript(channel));
const syncData = browserInterceptor.parseCallData(raw);

const applyCalls = (data: MockApplyData) => {
const calls = data.calls ?? [];
const results = data.results ?? [];
const invocationCallOrder = data.invocationCallOrder ?? [];
(originalMock.calls as unknown[][]).length = 0;
(originalMock.results as { type: string; value: unknown }[]).length = 0;
(originalMock.invocationCallOrder as number[]).length = 0;
for (let i = 0; i < syncData.calls.length; i++) {
(originalMock.calls as unknown[][]).push(syncData.calls[i]);
for (let i = 0; i < calls.length; i++) {
(originalMock.calls as unknown[][]).push(calls[i]);
(originalMock.results as { type: string; value: unknown }[]).push(
syncData.results[i] ?? { type: 'return', value: undefined },
results[i] ?? { type: 'return', value: undefined },
);
(originalMock.invocationCallOrder as number[]).push(
syncData.invocationCallOrder[i] ?? originalMock.invocationCallOrder.length,
invocationCallOrder[i] ?? originalMock.invocationCallOrder.length,
);
}
};

mock.__accessor = { kind: 'browser', channel } satisfies MockReadAccessor;
mock.__applyCalls = applyCalls;

mock.update = async () => {
const raw = await runInterceptorScript<unknown>(browser, browserInterceptor.buildCallDataReadScript(channel));
const syncData = browserInterceptor.parseCallData(raw);
applyCalls(syncData);
return mock;
};

Expand Down
15 changes: 15 additions & 0 deletions packages/electron-service/src/mockFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ export type MockAccessor =
| { kind: 'api'; apiName: string; funcName: string }
| { kind: 'prototype'; className: string; methodName: string };

/**
* Shape of mock call data delivered to a mock's `__applyCalls()` by the
* batched scheduler. Each mock variant uses what it needs and ignores the
* rest (API mocks ignore results; browser-mode mocks consume invocationCallOrder).
*/
export interface MockApplyData {
calls: unknown[][];
results?: Array<{ type: string; value: unknown }>;
invocationCallOrder?: number[];
}

// Re-export the user-facing accessor type from native-types under the local
// `MockReadAccessor` alias for use across service modules.
export type { ElectronMockReadAccessor as MockReadAccessor } from '@wdio/native-types';

// ============================================================================
// Restore helper for API-function mocks
// ============================================================================
Expand Down
Loading
Loading