Skip to content

Commit 326f610

Browse files
committed
Split unit & e2e testing
1 parent f49e859 commit 326f610

5 files changed

Lines changed: 120 additions & 42 deletions

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
"start:dev": "tsx ./build-setup/skip-server.ts && cross-env HTK_DEV=true APP_URL='http://localhost:8080' npm run start:app",
2020
"start:app": "tsc-watch --onSuccess \"electron .\"",
2121
"build:cli": "tsx ./build-setup/build-cli.ts",
22-
"test": "npm run server:setup && npm run build:src && playwright test"
22+
"test": "npm run server:setup && npm run build:src && npm run test:unit && npm run test:smoke",
23+
"test:unit": "node --experimental-strip-types --import ./test/register-stubs.js --test test/ui-bridge.spec.ts",
24+
"test:smoke": "playwright test"
2325
},
2426
"keywords": [],
2527
"author": "Tim Perry",

playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { defineConfig } from '@playwright/test';
22

33
export default defineConfig({
44
testDir: './test',
5+
testMatch: 'smoke.spec.ts',
56
timeout: 30000,
67
workers: 1,
78
retries: 0,

test/electron-stub-loader.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// ESM loader hook that stubs the 'electron' module and '@sentry/electron/main'
2+
// when running in plain Node.js (outside Electron). This allows test files to
3+
// import build output that transitively depends on these packages without
4+
// hitting import errors or Electron-specific initialization failures.
5+
6+
const ELECTRON_STUB_URL = 'stub://electron';
7+
const SENTRY_STUB_URL = 'stub://sentry-electron';
8+
9+
export function resolve(specifier, context, nextResolve) {
10+
if (specifier === 'electron') {
11+
return { url: ELECTRON_STUB_URL, shortCircuit: true };
12+
}
13+
if (specifier === '@sentry/electron/main' || specifier === '@sentry/electron') {
14+
return { url: SENTRY_STUB_URL, shortCircuit: true };
15+
}
16+
return nextResolve(specifier, context);
17+
}
18+
19+
export function load(url, context, nextLoad) {
20+
if (url === ELECTRON_STUB_URL) {
21+
return { format: 'module', source: ELECTRON_STUB, shortCircuit: true };
22+
}
23+
if (url === SENTRY_STUB_URL) {
24+
return { format: 'module', source: SENTRY_STUB, shortCircuit: true };
25+
}
26+
return nextLoad(url, context);
27+
}
28+
29+
// Stub for @sentry/electron/main — provides the API surface used by errors.ts
30+
// as no-ops, avoiding all Electron-specific Sentry initialization.
31+
const SENTRY_STUB = `
32+
export function init() {}
33+
export function rewriteFramesIntegration() { return {}; }
34+
export function setUser() {}
35+
export function captureMessage() {}
36+
export function captureException() {}
37+
export function addBreadcrumb() {}
38+
`;
39+
40+
// Stub for the 'electron' module — a recursive Proxy that silently absorbs
41+
// any property access or function call, as a safety net for any other code
42+
// that imports electron outside of the Electron runtime.
43+
const ELECTRON_STUB = `
44+
const noop = () => {};
45+
const handler = {
46+
get: (_, prop) => {
47+
if (typeof prop === 'symbol') return undefined;
48+
return new Proxy(noop, handler);
49+
},
50+
apply: () => new Proxy(noop, handler)
51+
};
52+
const stub = new Proxy(noop, handler);
53+
54+
export default stub;
55+
export const app = stub;
56+
export const BrowserWindow = stub;
57+
export const Menu = stub;
58+
export const dialog = stub;
59+
export const session = stub;
60+
export const ipcMain = stub;
61+
export const shell = stub;
62+
export const net = stub;
63+
export const protocol = stub;
64+
export const webContents = stub;
65+
export const crashReporter = stub;
66+
export const powerMonitor = stub;
67+
export const screen = stub;
68+
export const autoUpdater = stub;
69+
export const Session = stub;
70+
export const MessageChannelMain = stub;
71+
`;

test/register-stubs.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Preload script that registers the ESM loader hooks for stubbing
2+
// electron and @sentry/electron when running outside Electron.
3+
// Use via: node --import ./test/register-stubs.js ...
4+
import { register } from 'node:module';
5+
register(new URL('./electron-stub-loader.js', import.meta.url));

test/ui-bridge.spec.ts

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import { test, expect } from '@playwright/test';
1+
import { describe, it, after } from 'node:test';
2+
import assert from 'node:assert/strict';
23
import { once } from 'events';
34
import { MessageChannel } from 'worker_threads';
45

56
import { UiBridge } from '../build/ui-bridge.js';
67

7-
const DUMMY_OPTIONS = {
8-
getSkillsMarkdown: () => ''
9-
};
10-
118
const DUMMY_OPS = [
129
{ name: 'test.echo', description: 'Echo', category: 'test', inputSchema: {} }
1310
];
@@ -47,32 +44,32 @@ async function sendOpsAndWaitForReady(
4744
await once(bridge, 'ready');
4845
}
4946

50-
test.describe('UiBridge', () => {
47+
describe('UiBridge', () => {
5148

52-
test('starts in not-ready state', () => {
53-
const bridge = new UiBridge(DUMMY_OPTIONS);
54-
expect(bridge.isReady).toBe(false);
55-
expect(bridge.currentOperations).toEqual([]);
49+
it('starts in not-ready state', () => {
50+
const bridge = new UiBridge();
51+
assert.equal(bridge.isReady, false);
52+
assert.deepEqual(bridge.currentOperations, []);
5653
bridge.destroy();
5754
});
5855

59-
test('transitions to ready on first operations message', async () => {
56+
it('transitions to ready on first operations message', async () => {
6057
const { bridgePort, rendererPort } = createTestChannel();
61-
const bridge = new UiBridge(DUMMY_OPTIONS);
58+
const bridge = new UiBridge();
6259
bridge.setPort(bridgePort as any);
6360

6461
rendererPort.postMessage({ type: 'operations', operations: DUMMY_OPS });
6562
await once(bridge, 'ready');
6663

67-
expect(bridge.isReady).toBe(true);
68-
expect(bridge.currentOperations).toEqual(DUMMY_OPS);
64+
assert.equal(bridge.isReady, true);
65+
assert.deepEqual(bridge.currentOperations, DUMMY_OPS);
6966
bridge.destroy();
7067
rendererPort.close();
7168
});
7269

73-
test('updates operations without re-emitting ready', async () => {
70+
it('updates operations without re-emitting ready', async () => {
7471
const { bridgePort, rendererPort } = createTestChannel();
75-
const bridge = new UiBridge(DUMMY_OPTIONS);
72+
const bridge = new UiBridge();
7673
bridge.setPort(bridgePort as any);
7774

7875
await sendOpsAndWaitForReady(rendererPort, bridge);
@@ -84,15 +81,15 @@ test.describe('UiBridge', () => {
8481
rendererPort.postMessage({ type: 'operations', operations: newOps });
8582
await once(bridge, 'operations-changed');
8683

87-
expect(bridge.currentOperations).toEqual(newOps);
88-
expect(readyCount).toBe(0);
84+
assert.deepEqual(bridge.currentOperations, newOps);
85+
assert.equal(readyCount, 0);
8986
bridge.destroy();
9087
rendererPort.close();
9188
});
9289

93-
test('executeOperation sends request and resolves on response', async () => {
90+
it('executeOperation sends request and resolves on response', async () => {
9491
const { bridgePort, rendererPort } = createTestChannel();
95-
const bridge = new UiBridge(DUMMY_OPTIONS);
92+
const bridge = new UiBridge();
9693
bridge.setPort(bridgePort as any);
9794

9895
await sendOpsAndWaitForReady(rendererPort, bridge);
@@ -101,10 +98,10 @@ test.describe('UiBridge', () => {
10198

10299
// Wait for the request to arrive at the renderer
103100
const [request] = await once(rendererPort, 'message');
104-
expect(request.type).toBe('request');
105-
expect(request.operation).toBe('test.echo');
106-
expect(request.params).toEqual({ msg: 'hello' });
107-
expect(request.id).toBeTruthy();
101+
assert.equal(request.type, 'request');
102+
assert.equal(request.operation, 'test.echo');
103+
assert.deepEqual(request.params, { msg: 'hello' });
104+
assert.ok(request.id);
108105

109106
rendererPort.postMessage({
110107
type: 'response',
@@ -113,14 +110,14 @@ test.describe('UiBridge', () => {
113110
});
114111

115112
const result = await resultPromise;
116-
expect(result).toEqual({ success: true, data: 'echoed' });
113+
assert.deepEqual(result, { success: true, data: 'echoed' });
117114
bridge.destroy();
118115
rendererPort.close();
119116
});
120117

121-
test('executeOperation rejects on error response', async () => {
118+
it('executeOperation rejects on error response', async () => {
122119
const { bridgePort, rendererPort } = createTestChannel();
123-
const bridge = new UiBridge(DUMMY_OPTIONS);
120+
const bridge = new UiBridge();
124121
bridge.setPort(bridgePort as any);
125122

126123
await sendOpsAndWaitForReady(rendererPort, bridge);
@@ -134,23 +131,25 @@ test.describe('UiBridge', () => {
134131
error: 'Something went wrong'
135132
});
136133

137-
await expect(resultPromise).rejects.toThrow('Something went wrong');
134+
await assert.rejects(resultPromise, { message: 'Something went wrong' });
138135
bridge.destroy();
139136
rendererPort.close();
140137
});
141138

142-
test('executeOperation rejects when not ready and no operations', async () => {
143-
const bridge = new UiBridge(DUMMY_OPTIONS);
139+
it('executeOperation rejects when not ready and no operations', async () => {
140+
const bridge = new UiBridge();
144141

145-
await expect(bridge.executeOperation('test.echo', {}))
146-
.rejects.toThrow('Renderer API is not ready');
142+
await assert.rejects(
143+
bridge.executeOperation('test.echo', {}),
144+
{ message: 'Renderer API is not ready' }
145+
);
147146
bridge.destroy();
148147
});
149148

150-
test('setPort rejects pending requests from old port', async () => {
149+
it('setPort rejects pending requests from old port', async () => {
151150
const ch1 = createTestChannel();
152151
const ch2 = createTestChannel();
153-
const bridge = new UiBridge(DUMMY_OPTIONS);
152+
const bridge = new UiBridge();
154153
bridge.setPort(ch1.bridgePort as any);
155154

156155
await sendOpsAndWaitForReady(ch1.rendererPort, bridge);
@@ -159,16 +158,16 @@ test.describe('UiBridge', () => {
159158

160159
bridge.setPort(ch2.bridgePort as any);
161160

162-
await expect(resultPromise).rejects.toThrow('Renderer disconnected');
163-
expect(bridge.isReady).toBe(false);
161+
await assert.rejects(resultPromise, { message: 'Renderer disconnected' });
162+
assert.equal(bridge.isReady, false);
164163
bridge.destroy();
165164
ch1.rendererPort.close();
166165
ch2.rendererPort.close();
167166
});
168167

169-
test('ignores responses for unknown request IDs', async () => {
168+
it('ignores responses for unknown request IDs', async () => {
170169
const { bridgePort, rendererPort } = createTestChannel();
171-
const bridge = new UiBridge(DUMMY_OPTIONS);
170+
const bridge = new UiBridge();
172171
bridge.setPort(bridgePort as any);
173172

174173
// Send an unknown response, then a valid operations message.
@@ -181,9 +180,9 @@ test.describe('UiBridge', () => {
181180
rendererPort.close();
182181
});
183182

184-
test('ignores unrecognized messages', async () => {
183+
it('ignores unrecognized messages', async () => {
185184
const { bridgePort, rendererPort } = createTestChannel();
186-
const bridge = new UiBridge(DUMMY_OPTIONS);
185+
const bridge = new UiBridge();
187186
bridge.setPort(bridgePort as any);
188187

189188
// Send unrecognized messages, then a valid one to confirm delivery.
@@ -193,7 +192,7 @@ test.describe('UiBridge', () => {
193192
await once(bridge, 'ready');
194193

195194
// Bridge should be ready (from the valid message) with no ill effects from the others
196-
expect(bridge.isReady).toBe(true);
195+
assert.equal(bridge.isReady, true);
197196
bridge.destroy();
198197
rendererPort.close();
199198
});

0 commit comments

Comments
 (0)