mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
271 lines
7.8 KiB
TypeScript
271 lines
7.8 KiB
TypeScript
import fs from 'node:fs';
|
|
import { afterEach, test, vi } from 'vitest';
|
|
import assert from 'node:assert/strict';
|
|
|
|
vi.mock('../utils/exec.ts', () => ({
|
|
runCmdStreaming: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../client-react-devtools-companion.ts', () => ({
|
|
ensureReactDevtoolsCompanion: vi.fn(),
|
|
stopReactDevtoolsCompanion: vi.fn(),
|
|
}));
|
|
|
|
import { runCmdStreaming } from '../utils/exec.ts';
|
|
import {
|
|
ensureReactDevtoolsCompanion,
|
|
stopReactDevtoolsCompanion,
|
|
} from '../client-react-devtools-companion.ts';
|
|
import {
|
|
AGENT_REACT_DEVTOOLS_PACKAGE,
|
|
buildReactDevtoolsNpmExecArgs,
|
|
runReactDevtoolsCommand,
|
|
} from '../cli/commands/react-devtools.ts';
|
|
|
|
type ReactDevtoolsOptions = NonNullable<Parameters<typeof runReactDevtoolsCommand>[1]>;
|
|
type ReactDevtoolsFlags = NonNullable<ReactDevtoolsOptions['flags']>;
|
|
|
|
const remoteBridgeScope = {
|
|
metroProxyBaseUrl: 'https://bridge.example.test',
|
|
metroBearerToken: 'token',
|
|
tenant: 'tenant-1',
|
|
runId: 'run-1',
|
|
leaseId: 'lease-1',
|
|
} as const;
|
|
|
|
const remoteBridgeBackends = [
|
|
{ label: 'Android', leaseBackend: 'android-instance' },
|
|
{ label: 'iOS', leaseBackend: 'ios-instance' },
|
|
] as const;
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
test('react-devtools passthrough pins agent-react-devtools package version', () => {
|
|
assert.equal(AGENT_REACT_DEVTOOLS_PACKAGE, 'agent-react-devtools@0.4.0');
|
|
assert.deepEqual(buildReactDevtoolsNpmExecArgs(['get', 'tree', '--depth', '3']), [
|
|
'exec',
|
|
'--yes',
|
|
'--package',
|
|
'agent-react-devtools@0.4.0',
|
|
'--',
|
|
'agent-react-devtools',
|
|
'get',
|
|
'tree',
|
|
'--depth',
|
|
'3',
|
|
]);
|
|
});
|
|
|
|
test('react-devtools docs mention the pinned package version', () => {
|
|
const docs = ['website/docs/docs/commands.md'];
|
|
|
|
for (const file of docs) {
|
|
assert.match(fs.readFileSync(file, 'utf8'), new RegExp(AGENT_REACT_DEVTOOLS_PACKAGE));
|
|
}
|
|
});
|
|
|
|
function mockRemoteCompanionSuccess(): void {
|
|
vi.mocked(runCmdStreaming).mockResolvedValueOnce({
|
|
exitCode: 0,
|
|
stdout: '',
|
|
stderr: '',
|
|
});
|
|
vi.mocked(ensureReactDevtoolsCompanion).mockResolvedValueOnce({
|
|
pid: 123,
|
|
spawned: true,
|
|
statePath: '/tmp/state.json',
|
|
logPath: '/tmp/companion.log',
|
|
});
|
|
}
|
|
|
|
function assertNoRemoteCompanion(): void {
|
|
assert.equal(vi.mocked(ensureReactDevtoolsCompanion).mock.calls.length, 0);
|
|
assert.equal(vi.mocked(stopReactDevtoolsCompanion).mock.calls.length, 0);
|
|
}
|
|
|
|
async function runStatusWithoutCompanion(flags: ReactDevtoolsFlags): Promise<void> {
|
|
vi.mocked(runCmdStreaming).mockResolvedValueOnce({
|
|
exitCode: 0,
|
|
stdout: '',
|
|
stderr: '',
|
|
});
|
|
|
|
await runReactDevtoolsCommand(['status'], {
|
|
stateDir: '/tmp/agent-device-state',
|
|
session: 'default',
|
|
flags,
|
|
});
|
|
|
|
assertNoRemoteCompanion();
|
|
}
|
|
|
|
function assertRemoteCompanionStarted(env: NodeJS.ProcessEnv): void {
|
|
assert.equal(vi.mocked(ensureReactDevtoolsCompanion).mock.calls.length, 1);
|
|
assert.deepEqual(vi.mocked(ensureReactDevtoolsCompanion).mock.calls[0]?.[0], {
|
|
projectRoot: '/tmp/project',
|
|
stateDir: '/tmp/agent-device-state',
|
|
serverBaseUrl: 'https://bridge.example.test',
|
|
bearerToken: 'token',
|
|
bridgeScope: {
|
|
tenantId: 'tenant-1',
|
|
runId: 'run-1',
|
|
leaseId: 'lease-1',
|
|
},
|
|
session: 'default',
|
|
profileKey: '/tmp/remote.json',
|
|
consumerKey: 'default',
|
|
env,
|
|
});
|
|
assert.equal(vi.mocked(runCmdStreaming).mock.calls[0]?.[0], 'npm');
|
|
assert.equal(vi.mocked(runCmdStreaming).mock.calls[0]?.[2]?.cwd, '/tmp/project');
|
|
assert.equal(vi.mocked(runCmdStreaming).mock.calls[0]?.[2]?.env, env);
|
|
assert.equal(vi.mocked(stopReactDevtoolsCompanion).mock.calls.length, 0);
|
|
}
|
|
|
|
for (const { label, leaseBackend } of remoteBridgeBackends) {
|
|
test(`react-devtools keeps remote ${label} companion after passthrough command`, async () => {
|
|
const env = { ...process.env };
|
|
mockRemoteCompanionSuccess();
|
|
|
|
const exitCode = await runReactDevtoolsCommand(['status'], {
|
|
stateDir: '/tmp/agent-device-state',
|
|
session: 'default',
|
|
cwd: '/tmp/project',
|
|
env,
|
|
flags: {
|
|
...remoteBridgeScope,
|
|
leaseBackend,
|
|
remoteConfig: '/tmp/remote.json',
|
|
session: 'default',
|
|
},
|
|
});
|
|
|
|
assert.equal(exitCode, 0);
|
|
assertRemoteCompanionStarted(env);
|
|
});
|
|
}
|
|
|
|
test('react-devtools wait hints to relaunch remote iOS after startup-time connection misses', async () => {
|
|
const env = { ...process.env };
|
|
let stderr = '';
|
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
stderr += chunk.toString();
|
|
return true;
|
|
}) as typeof process.stderr.write;
|
|
vi.mocked(runCmdStreaming).mockResolvedValueOnce({
|
|
exitCode: 1,
|
|
stdout: '',
|
|
stderr: '',
|
|
});
|
|
vi.mocked(ensureReactDevtoolsCompanion).mockResolvedValueOnce({
|
|
pid: 123,
|
|
spawned: true,
|
|
statePath: '/tmp/state.json',
|
|
logPath: '/tmp/companion.log',
|
|
});
|
|
|
|
try {
|
|
const exitCode = await runReactDevtoolsCommand(['wait', '--connected'], {
|
|
stateDir: '/tmp/agent-device-state',
|
|
session: 'default',
|
|
cwd: '/tmp/project',
|
|
env,
|
|
flags: {
|
|
...remoteBridgeScope,
|
|
leaseBackend: 'ios-instance',
|
|
remoteConfig: '/tmp/remote.json',
|
|
session: 'default',
|
|
},
|
|
});
|
|
|
|
assert.equal(exitCode, 1);
|
|
assert.match(stderr, /Remote iOS React DevTools connects during JavaScript startup/);
|
|
assert.match(stderr, /open <bundle-id> --platform ios --relaunch/);
|
|
} finally {
|
|
process.stderr.write = originalStderrWrite;
|
|
}
|
|
});
|
|
|
|
test('react-devtools stop cleans up remote companion', async () => {
|
|
const env = { ...process.env };
|
|
vi.mocked(runCmdStreaming).mockResolvedValueOnce({
|
|
exitCode: 0,
|
|
stdout: '',
|
|
stderr: '',
|
|
});
|
|
vi.mocked(stopReactDevtoolsCompanion).mockResolvedValueOnce({
|
|
stopped: true,
|
|
statePath: '/tmp/state.json',
|
|
});
|
|
|
|
const exitCode = await runReactDevtoolsCommand(['stop'], {
|
|
stateDir: '/tmp/agent-device-state',
|
|
session: 'default',
|
|
cwd: '/tmp/project',
|
|
env,
|
|
flags: {
|
|
...remoteBridgeScope,
|
|
leaseBackend: 'ios-instance',
|
|
remoteConfig: '/tmp/remote.json',
|
|
session: 'default',
|
|
},
|
|
});
|
|
|
|
assert.equal(exitCode, 0);
|
|
assert.equal(vi.mocked(ensureReactDevtoolsCompanion).mock.calls.length, 0);
|
|
assert.equal(vi.mocked(runCmdStreaming).mock.calls[0]?.[0], 'npm');
|
|
assert.deepEqual(vi.mocked(runCmdStreaming).mock.calls[0]?.[1], [
|
|
'exec',
|
|
'--yes',
|
|
'--package',
|
|
'agent-react-devtools@0.4.0',
|
|
'--',
|
|
'agent-react-devtools',
|
|
'stop',
|
|
]);
|
|
assert.equal(vi.mocked(stopReactDevtoolsCompanion).mock.calls.length, 1);
|
|
assert.deepEqual(vi.mocked(stopReactDevtoolsCompanion).mock.calls[0]?.[0], {
|
|
projectRoot: '/tmp/project',
|
|
stateDir: '/tmp/agent-device-state',
|
|
profileKey: '/tmp/remote.json',
|
|
consumerKey: 'default',
|
|
});
|
|
});
|
|
|
|
test('react-devtools skips companion for non-bridge remote sessions', async () => {
|
|
await runStatusWithoutCompanion({
|
|
...remoteBridgeScope,
|
|
leaseBackend: 'ios-simulator',
|
|
});
|
|
});
|
|
|
|
test('react-devtools skips companion when remote bridge backend is missing', async () => {
|
|
await runStatusWithoutCompanion(remoteBridgeScope);
|
|
});
|
|
|
|
for (const { label, leaseBackend } of remoteBridgeBackends) {
|
|
test(`react-devtools fails clearly when remote ${label} bridge scope is incomplete`, async () => {
|
|
await assert.rejects(
|
|
() =>
|
|
runReactDevtoolsCommand(['status'], {
|
|
stateDir: '/tmp/agent-device-state',
|
|
session: 'default',
|
|
flags: {
|
|
leaseBackend,
|
|
metroProxyBaseUrl: 'https://bridge.example.test',
|
|
tenant: 'tenant-1',
|
|
runId: 'run-1',
|
|
leaseId: 'lease-1',
|
|
},
|
|
}),
|
|
/react-devtools remote bridge requires metroBearerToken/,
|
|
);
|
|
|
|
assert.equal(vi.mocked(runCmdStreaming).mock.calls.length, 0);
|
|
assertNoRemoteCompanion();
|
|
});
|
|
}
|