Skip to content
Merged
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
6 changes: 4 additions & 2 deletions e2e/browser-mode/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ describe('browser mode - basic', () => {

await expectExecSuccess();
expect(cli.stdout).toMatch(/Test Files.*passed/);
expect(cli.stdout).toContain('/scheduler.html');
});

it('should exit with code 0 when tests pass', async () => {
it('should run headed mode without scheduler page and exit with code 0', async () => {
const { cli } = await runBrowserCli('basic', {
args: ['tests/dom.test.ts'],
args: ['--browser.headless', 'false', 'tests/dom.test.ts'],
});

await cli.exec;
expect(cli.exec.exitCode).toBe(0);
expect(cli.stdout).not.toContain('/scheduler.html');
});
});
1 change: 1 addition & 0 deletions packages/browser-ui/rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default defineConfig({
source: {
entry: {
index: './src/main.tsx',
scheduler: './src/scheduler.ts',
},
},
output: {
Expand Down
81 changes: 81 additions & 0 deletions packages/browser-ui/src/core/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type {
BrowserClientMessage,
HostRPC,
SnapshotRpcRequest,
SnapshotRpcResponse,
} from '../types';

const DISPATCH_MESSAGE_TYPE = '__rstest_dispatch__';
const SNAPSHOT_RESPONSE_TYPE = '__rstest_snapshot_response__';

type SnapshotRpcHandler = Pick<
HostRPC,
| 'resolveSnapshotPath'
| 'readSnapshotFile'
| 'saveSnapshotFile'
| 'removeSnapshotFile'
>;

const canPostMessage = (
sourceWindow: MessageEventSource | null,
): sourceWindow is Window => {
return (
sourceWindow !== null &&
typeof (sourceWindow as Window).postMessage === 'function'
);
};

export const readDispatchMessage = (
event: MessageEvent,
): BrowserClientMessage | null => {
if (event.data?.type !== DISPATCH_MESSAGE_TYPE) {
return null;
}
return event.data.payload as BrowserClientMessage;
};

export const forwardSnapshotRpcRequest = async (
rpc: SnapshotRpcHandler | null | undefined,
request: SnapshotRpcRequest,
sourceWindow: MessageEventSource | null,
): Promise<void> => {
if (!rpc || !canPostMessage(sourceWindow)) {
return;
}

const sendResponse = (response: SnapshotRpcResponse) => {
sourceWindow.postMessage(
{ type: SNAPSHOT_RESPONSE_TYPE, payload: response },
'*',
);
};

try {
let result: unknown;
switch (request.method) {
case 'resolveSnapshotPath':
result = await rpc.resolveSnapshotPath(request.args.testPath);
break;
case 'readSnapshotFile':
result = await rpc.readSnapshotFile(request.args.filepath);
break;
case 'saveSnapshotFile':
result = await rpc.saveSnapshotFile(
request.args.filepath,
request.args.content,
);
break;
case 'removeSnapshotFile':
result = await rpc.removeSnapshotFile(request.args.filepath);
break;
default:
result = undefined;
}
sendResponse({ id: request.id, result });
} catch (error) {
sendResponse({
id: request.id,
error: error instanceof Error ? error.message : String(error),
});
}
};
24 changes: 24 additions & 0 deletions packages/browser-ui/src/core/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16_000, 30_000];

export const createWebSocketUrl = (wsPort: number): string => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${protocol}//${window.location.hostname}:${wsPort}`;
};

export const createRunnerUrl = (
testFile: string,
runnerBase?: string,
testNamePattern?: string,
cacheBust = false,
): string => {
const base = runnerBase || window.location.origin;
const url = new URL('/runner.html', base);
url.searchParams.set('testFile', testFile);
if (testNamePattern) {
url.searchParams.set('testNamePattern', testNamePattern);
}
if (cacheBust) {
url.searchParams.set('t', Date.now().toString());
}
return url.toString();
};
59 changes: 3 additions & 56 deletions packages/browser-ui/src/hooks/useRpc.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,9 @@
import { type BirpcReturn, createBirpc } from 'birpc';
import { useEffect, useRef, useState } from 'react';
import type {
BrowserClientFileResult,
BrowserClientTestResult,
TestFileInfo,
} from '../types';
import { createWebSocketUrl, RECONNECT_DELAYS } from '../core/runtime';
import type { ContainerRPC, HostRPC, TestFileInfo } from '../types';
import { logger } from '../utils/logger';

// ============================================================================
// RPC Types
// ============================================================================

/** Payload for test file start event */
export type TestFileStartPayload = {
testPath: string;
projectName: string;
};

/** Payload for log event */
export type LogPayload = {
level: 'log' | 'warn' | 'error' | 'info' | 'debug';
content: string;
testPath: string;
type: 'stdout' | 'stderr';
trace?: string;
};

/** Payload for fatal error event */
export type FatalPayload = {
message: string;
stack?: string;
};

export type HostRPC = {
rerunTest: (testFile: string, testNamePattern?: string) => Promise<void>;
getTestFiles: () => Promise<TestFileInfo[]>;
// Test result callbacks from container
onTestFileStart: (payload: TestFileStartPayload) => Promise<void>;
onTestCaseResult: (payload: BrowserClientTestResult) => Promise<void>;
onTestFileComplete: (payload: BrowserClientFileResult) => Promise<void>;
onLog: (payload: LogPayload) => Promise<void>;
onFatal: (payload: FatalPayload) => Promise<void>;
// Snapshot file operations (for browser mode snapshot support)
resolveSnapshotPath: (testPath: string) => Promise<string>;
readSnapshotFile: (filepath: string) => Promise<string | null>;
saveSnapshotFile: (filepath: string, content: string) => Promise<void>;
removeSnapshotFile: (filepath: string) => Promise<void>;
};

export type ContainerRPC = {
onTestFileUpdate: (testFiles: TestFileInfo[]) => void;
reloadTestFile: (testFile: string, testNamePattern?: string) => void;
};

export type RpcState = {
rpc: BirpcReturn<HostRPC, ContainerRPC> | null;
loading: boolean;
Expand All @@ -63,8 +14,6 @@ export type RpcState = {
// useRpc Hook - WebSocket connection with reconnect logic
// ============================================================================

const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000, 30000]; // Exponential backoff, max 30s

export const useRpc = (
setTestFiles: (files: TestFileInfo[]) => void,
wsPort: number | undefined,
Expand Down Expand Up @@ -104,9 +53,7 @@ export const useRpc = (
const connect = () => {
if (!isMounted) return;

const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.hostname}:${wsPort}`;
ws = new WebSocket(wsUrl);
ws = new WebSocket(createWebSocketUrl(wsPort));
activeWsRef.current = ws;

const methods: ContainerRPC = {
Expand Down
Loading
Loading