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
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1785423581556.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
__default__: patch
---

The web and Vega platforms start correctly again. Session setup previously threw `Cannot read properties of undefined (reading 'aborted')` because these runners received the harness config in place of their init options.
14 changes: 5 additions & 9 deletions packages/platform-android/src/runner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
HarnessPlatformRunner,
type HarnessPlatformInitOptions,
} from '@react-native-harness/platforms';
import { type HarnessPlatformRunnerFactory } from '@react-native-harness/platforms';
import type { Config as HarnessConfig } from '@react-native-harness/config';
import {
AndroidPlatformConfigSchema,
Expand All @@ -17,11 +14,10 @@ import {
initializeAndroidProcessEnv,
} from './environment.js';

const getAndroidRunner = async (
config: AndroidPlatformConfig,
harnessConfig: HarnessConfig,
init: HarnessPlatformInitOptions
): Promise<HarnessPlatformRunner> => {
const getAndroidRunner: HarnessPlatformRunnerFactory<
AndroidPlatformConfig,
HarnessConfig
> = async (config, harnessConfig, init) => {
const parsedConfig = AndroidPlatformConfigSchema.parse(config);

initializeAndroidProcessEnv();
Expand Down
14 changes: 5 additions & 9 deletions packages/platform-ios/src/runner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
HarnessPlatformRunner,
type HarnessPlatformInitOptions,
} from '@react-native-harness/platforms';
import { type HarnessPlatformRunnerFactory } from '@react-native-harness/platforms';
import type { Config as HarnessConfig } from '@react-native-harness/config';
import {
ApplePlatformConfigSchema,
Expand All @@ -13,11 +10,10 @@ import {
getAppleSimulatorPlatformInstance,
} from './instance.js';

const getAppleRunner = async (
config: ApplePlatformConfig,
harnessConfig: HarnessConfig,
init: HarnessPlatformInitOptions
): Promise<HarnessPlatformRunner> => {
const getAppleRunner: HarnessPlatformRunnerFactory<
ApplePlatformConfig,
HarnessConfig
> = async (config, harnessConfig, init) => {
const parsedConfig = ApplePlatformConfigSchema.parse(config);

if (isAppleDeviceSimulator(parsedConfig.device)) {
Expand Down
6 changes: 5 additions & 1 deletion packages/platform-vega/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export default [
'@nx/dependency-checks': [
'error',
{
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
ignoredDependencies: ['vite', 'vitest'],
ignoredFiles: [
'{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
'{projectRoot}/src/**/__tests__/**',
],
},
],
},
Expand Down
1 change: 1 addition & 0 deletions packages/platform-vega/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
}
},
"dependencies": {
"@react-native-harness/config": "workspace:*",
"@react-native-harness/platforms": "workspace:*",
"@react-native-harness/tools": "workspace:*",
"zod": "^3.25.67",
Expand Down
57 changes: 57 additions & 0 deletions packages/platform-vega/src/__tests__/runner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it, vi } from 'vitest';
import {
DEFAULT_METRO_PORT,
type Config as HarnessConfig,
} from '@react-native-harness/config';
import type { VegaPlatformConfig } from '../config.js';

const stopAppMock = vi.fn().mockResolvedValue(undefined);

vi.mock('../kepler.js', () => ({
getVegaDeviceStatus: vi.fn().mockResolvedValue('running'),
isAppInstalled: vi.fn().mockResolvedValue(true),
isAppRunning: vi.fn().mockResolvedValue(false),
startApp: vi.fn().mockResolvedValue(undefined),
stopApp: stopAppMock,
}));

const harnessConfig = { metroPort: DEFAULT_METRO_PORT } as HarnessConfig;

const config: VegaPlatformConfig = {
name: 'vega',
device: { type: 'emulator', deviceId: 'VegaTV_1' },
bundleId: 'com.example.app',
};

describe('getVegaRunner', () => {
it('is invoked with the (config, harnessConfig, init) shape the harness session uses', async () => {
// Regression test for the bug where the session called
// module.default(config, runtimeConfig, init), but this runner declared
// only (config, init?) — so runtimeConfig landed in the init slot and
// init.signal.aborted threw. Asserting the arity here catches an
// optional-parameter regression, since optional params drop out of
// Function.length.
const { default: getVegaRunner } = await import('../runner.js');
expect(getVegaRunner.length).toBe(3);

const init = { signal: new AbortController().signal };
const runner = await getVegaRunner(config, harnessConfig, init);
expect(runner.createAppSession).toBeTypeOf('function');
expect(runner.dispose).toBeTypeOf('function');
});

it('stops the app when the init signal aborts after session creation', async () => {
const { default: getVegaRunner } = await import('../runner.js');
const controller = new AbortController();

const runner = await getVegaRunner(config, harnessConfig, {
signal: controller.signal,
});
await runner.createAppSession();
stopAppMock.mockClear();

controller.abort();

expect(stopAppMock).toHaveBeenCalled();
});
});
16 changes: 8 additions & 8 deletions packages/platform-vega/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
type AppSessionState,
DeviceNotFoundError,
AppNotInstalledError,
type HarnessPlatformInitOptions,
HarnessPlatformRunner,
type HarnessPlatformRunnerFactory,
} from '@react-native-harness/platforms';
import type { Config as HarnessConfig } from '@react-native-harness/config';
import { VegaPlatformConfigSchema, type VegaPlatformConfig } from './config.js';
import * as kepler from './kepler.js';

Expand All @@ -15,10 +15,10 @@ const APP_EXIT_POLL_INTERVAL_MS = 1000;
const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));

const getVegaRunner = async (
config: VegaPlatformConfig,
init?: HarnessPlatformInitOptions
): Promise<HarnessPlatformRunner> => {
const getVegaRunner: HarnessPlatformRunnerFactory<
VegaPlatformConfig,
HarnessConfig
> = async (config, _harnessConfig, init) => {
const parsedConfig = VegaPlatformConfigSchema.parse(config);
const deviceId = parsedConfig.device.deviceId;
const bundleId = parsedConfig.bundleId;
Expand All @@ -40,10 +40,10 @@ const getVegaRunner = async (
// poll loop and the app on session teardown, in addition to the normal
// dispose() path.
const disposeCurrentAppSessionOnAbort = () => void currentAppSession?.dispose();
if (init?.signal.aborted) {
if (init.signal.aborted) {
disposeCurrentAppSessionOnAbort();
} else {
init?.signal.addEventListener('abort', disposeCurrentAppSessionOnAbort, {
init.signal.addEventListener('abort', disposeCurrentAppSessionOnAbort, {
once: true,
});
}
Expand Down
3 changes: 3 additions & 0 deletions packages/platform-vega/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
{
"path": "../platforms"
},
{
"path": "../config"
},
{
"path": "./tsconfig.lib.json"
}
Expand Down
3 changes: 3 additions & 0 deletions packages/platform-vega/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
},
{
"path": "../platforms/tsconfig.lib.json"
},
{
"path": "../config/tsconfig.lib.json"
}
]
}
18 changes: 18 additions & 0 deletions packages/platform-vega/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';

export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/platform-vega',
test: {
watch: false,
globals: true,
environment: 'node',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
},
},
}));
6 changes: 5 additions & 1 deletion packages/platform-web/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export default [
'@nx/dependency-checks': [
'error',
{
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
ignoredDependencies: ['vite', 'vitest'],
ignoredFiles: [
'{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
'{projectRoot}/src/**/__tests__/**',
],
},
],
},
Expand Down
1 change: 1 addition & 0 deletions packages/platform-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
}
},
"dependencies": {
"@react-native-harness/config": "workspace:*",
"@react-native-harness/platforms": "workspace:*",
"playwright": "^1.50.0",
"zod": "^3.25.67",
Expand Down
89 changes: 89 additions & 0 deletions packages/platform-web/src/__tests__/runner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, it, vi } from 'vitest';
import {
DEFAULT_METRO_PORT,
type Config as HarnessConfig,
} from '@react-native-harness/config';
import type { WebPlatformConfig } from '../config.js';

const closeMock = vi.fn().mockResolvedValue(undefined);
const exposeFunctionMock = vi.fn().mockResolvedValue(undefined);
const gotoMock = vi.fn().mockResolvedValue(undefined);
const pageOnMock = vi.fn();

const launchMock = vi.fn().mockResolvedValue({
close: closeMock,
newContext: vi.fn().mockResolvedValue({
newPage: vi.fn().mockResolvedValue({
exposeFunction: exposeFunctionMock,
goto: gotoMock,
on: pageOnMock,
}),
}),
});

vi.mock('playwright', () => ({
chromium: { launch: launchMock },
firefox: { launch: launchMock },
webkit: { launch: launchMock },
}));

const harnessConfig = { metroPort: DEFAULT_METRO_PORT } as HarnessConfig;

const config: WebPlatformConfig = {
name: 'web',
browser: {
type: 'chromium',
url: 'http://localhost:8081',
headless: true,
},
};

describe('getWebRunner', () => {
it('is invoked with the (config, harnessConfig, init) shape the harness session uses', async () => {
// Regression test for the bug where the session called
// module.default(config, runtimeConfig, init), but this runner declared
// only (config, init?) — so runtimeConfig landed in the init slot and
// init.signal.aborted threw. Asserting the arity here catches an
// optional-parameter regression, since optional params drop out of
// Function.length.
const { default: getWebRunner } = await import('../runner.js');
expect(getWebRunner.length).toBe(3);

const init = { signal: new AbortController().signal };
const runner = await getWebRunner(config, harnessConfig, init);
expect(runner.createAppSession).toBeTypeOf('function');
expect(runner.dispose).toBeTypeOf('function');
});

it('does not throw when the init signal is already aborted', async () => {
// This is the exact failure mode from the reported bug: with the wrong
// argument order, `init` ended up holding runtimeConfig, and
// `init.signal.aborted` threw `TypeError: Cannot read properties of
// undefined (reading 'aborted')`.
const { default: getWebRunner } = await import('../runner.js');
const controller = new AbortController();
controller.abort();

const runner = await getWebRunner(config, harnessConfig, {
signal: controller.signal,
});
const session = await runner.createAppSession();

expect(session.dispose).toBeTypeOf('function');
});

it('closes the browser when the init signal aborts after session creation', async () => {
const { default: getWebRunner } = await import('../runner.js');
const controller = new AbortController();

const runner = await getWebRunner(config, harnessConfig, {
signal: controller.signal,
});
await runner.createAppSession();
closeMock.mockClear();

controller.abort();

expect(closeMock).toHaveBeenCalled();
});
});
16 changes: 8 additions & 8 deletions packages/platform-web/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import {
createAppSessionEmitter,
type AppSession,
type AppSessionState,
type HarnessPlatformInitOptions,
HarnessPlatformRunner,
type HarnessPlatformRunnerFactory,
} from '@react-native-harness/platforms';
import type { Config as HarnessConfig } from '@react-native-harness/config';
import { chromium, firefox, webkit, type Browser, type Page } from 'playwright';
import { WebPlatformConfigSchema, type WebPlatformConfig } from './config.js';

const getWebRunner = async (
config: WebPlatformConfig,
init?: HarnessPlatformInitOptions
): Promise<HarnessPlatformRunner> => {
const getWebRunner: HarnessPlatformRunnerFactory<
WebPlatformConfig,
HarnessConfig
> = async (config, _harnessConfig, init) => {
const parsedConfig = WebPlatformConfigSchema.parse(config);

let browser: Browser | null = null;
Expand All @@ -24,10 +24,10 @@ const getWebRunner = async (
browser = null;
page = null;
};
if (init?.signal.aborted) {
if (init.signal.aborted) {
closeBrowserOnAbort();
} else {
init?.signal.addEventListener('abort', closeBrowserOnAbort, { once: true });
init.signal.addEventListener('abort', closeBrowserOnAbort, { once: true });
}

const launchBrowser = async () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/platform-web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
{
"path": "../platforms"
},
{
"path": "../config"
},
{
"path": "./tsconfig.lib.json"
}
Expand Down
3 changes: 3 additions & 0 deletions packages/platform-web/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"references": [
{
"path": "../platforms/tsconfig.lib.json"
},
{
"path": "../config/tsconfig.lib.json"
}
]
}
18 changes: 18 additions & 0 deletions packages/platform-web/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';

export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/platform-web',
test: {
watch: false,
globals: true,
environment: 'node',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
},
},
}));
Loading
Loading