mirror of
https://github.com/hanzoai/Detox.git
synced 2026-09-24 16:55:12 +00:00
feat: new global lifecycle for Detox (#3333)
This commit is contained in:
committed by
GitHub
parent
de7819d946
commit
052dc258ee
@@ -1,4 +1,6 @@
|
||||
jest.mock('proper-lockfile');
|
||||
jest.mock('signal-exit');
|
||||
jest.mock('../src/logger/DetoxLogger');
|
||||
|
||||
const path = require('path');
|
||||
|
||||
@@ -36,5 +38,5 @@ function callCli(modulePath, cmd) {
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
global.callCli = callCli;
|
||||
global.IS_RUNNING_DETOX_UNIT_TESTS = true;
|
||||
|
||||
Vendored
+166
-63
@@ -8,6 +8,8 @@
|
||||
// * Max Komarychev <https://github.com/maxkomarychev>
|
||||
// * Dor Ben Baruch <https://github.com/Dor256>
|
||||
|
||||
import { BunyanDebugStreamOptions } from 'bunyan-debug-stream';
|
||||
|
||||
declare global {
|
||||
const device: Detox.DetoxExportWrapper['device'];
|
||||
const element: Detox.DetoxExportWrapper['element'];
|
||||
@@ -36,21 +38,7 @@ declare global {
|
||||
* @example extends: '@my-org/detox-preset'
|
||||
*/
|
||||
extends?: string;
|
||||
/**
|
||||
* Override test runner binary command.
|
||||
* @default 'jest'
|
||||
*/
|
||||
testRunner?: string;
|
||||
/**
|
||||
* @example runnerConfig: 'e2e/config.js'
|
||||
*/
|
||||
runnerConfig?: string;
|
||||
/**
|
||||
* Optional. A default glob pattern for a test runner to use when no test files are specified.
|
||||
*
|
||||
* @example specs: 'detoxE2E'
|
||||
*/
|
||||
specs?: string;
|
||||
|
||||
apps?: Record<string, DetoxAppConfig>;
|
||||
devices?: Record<string, DetoxDeviceConfig>;
|
||||
selectedConfiguration?: string;
|
||||
@@ -60,7 +48,9 @@ declare global {
|
||||
type DetoxConfigurationCommon = {
|
||||
artifacts?: false | DetoxArtifactsConfig;
|
||||
behavior?: DetoxBehaviorConfig;
|
||||
logger?: DetoxLoggerConfig;
|
||||
session?: DetoxSessionConfig;
|
||||
testRunner?: DetoxTestRunnerConfig;
|
||||
};
|
||||
|
||||
interface DetoxArtifactsConfig {
|
||||
@@ -71,7 +61,6 @@ declare global {
|
||||
screenshot?: 'none' | 'manual' | 'failing' | 'all' | DetoxScreenshotArtifactsPluginConfig;
|
||||
video?: 'none' | 'failing' | 'all' | DetoxVideoArtifactsPluginConfig;
|
||||
instruments?: 'none' | 'all' | DetoxInstrumentsArtifactsPluginConfig;
|
||||
timeline?: 'none' | 'all' | DetoxTimelineArtifactsPluginConfig;
|
||||
uiHierarchy?: 'disabled' | 'enabled' | DetoxUIHierarchyArtifactsPluginConfig;
|
||||
|
||||
[pluginId: string]: unknown;
|
||||
@@ -91,11 +80,19 @@ declare global {
|
||||
*/
|
||||
exposeGlobals?: boolean;
|
||||
/**
|
||||
* By default, `await detox.init()` will uninstall and install the app.
|
||||
* By default, Detox will uninstall and install the app upon the initialization.
|
||||
* If you wish to reuse the existing app for a faster run, set the property to
|
||||
* `false`.
|
||||
*/
|
||||
reinstallApp?: boolean;
|
||||
/**
|
||||
* If you wish to run multiple "detox test" commands in parallel,
|
||||
* make sure they don't delete the shared lock file – only the
|
||||
* first command should reset the lock file.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
keepLockFile?: boolean;
|
||||
};
|
||||
launchApp?: 'auto' | 'manual';
|
||||
cleanup?: {
|
||||
@@ -103,6 +100,15 @@ declare global {
|
||||
};
|
||||
}
|
||||
|
||||
interface DetoxLoggerConfig {
|
||||
level?: DetoxLogLevel;
|
||||
/**
|
||||
* @default 'sandbox'
|
||||
*/
|
||||
overrideConsole?: 'all' | 'sandbox' | 'none';
|
||||
options?: BunyanDebugStreamOptions | ((config: Partial<DetoxLoggerConfig>) => BunyanDebugStreamOptions);
|
||||
}
|
||||
|
||||
interface DetoxSessionConfig {
|
||||
autoStart?: boolean;
|
||||
debugSynchronization?: number;
|
||||
@@ -110,6 +116,49 @@ declare global {
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
interface DetoxTestRunnerConfig {
|
||||
args?: {
|
||||
/**
|
||||
* The command to use for runner: 'jest', 'nyc jest',
|
||||
*/
|
||||
$0: string;
|
||||
/**
|
||||
* The positional arguments to pass to the runner.
|
||||
*/
|
||||
_?: string[];
|
||||
/**
|
||||
* Any other properties recognized by test runner
|
||||
*/
|
||||
[prop: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* Configuration of custom integration features
|
||||
* between Detox and Jest
|
||||
*/
|
||||
jest?: {
|
||||
/**
|
||||
* Device init timeout
|
||||
*/
|
||||
initTimeout?: number | undefined;
|
||||
/**
|
||||
* Insist on CLI-based retry mechanism even when the failed tests have been handled
|
||||
* by jest.retryTimes(n) mechanism from Jest Circus.
|
||||
*/
|
||||
retryAfterCircusRetries?: boolean;
|
||||
reportSpecs?: boolean | undefined;
|
||||
reportWorkerAssign?: boolean | undefined;
|
||||
};
|
||||
/**
|
||||
* Retries count. Zero means a single attempt to run tests.
|
||||
*/
|
||||
retries?: number;
|
||||
/**
|
||||
* Custom handler to process --inspect-brk CLI flag.
|
||||
* Use it when you rely on another test runner than Jest.
|
||||
*/
|
||||
inspectBrk?: boolean | ((config: DetoxTestRunnerConfig) => void);
|
||||
}
|
||||
|
||||
type DetoxAppConfig = (DetoxBuiltInAppConfig | DetoxCustomAppConfig) & {
|
||||
/**
|
||||
* App name to use with device.selectApp(appName) calls.
|
||||
@@ -161,10 +210,6 @@ declare global {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface DetoxTimelineArtifactsPluginConfig {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
type DetoxBuiltInAppConfig = (DetoxIosAppConfig | DetoxAndroidAppConfig);
|
||||
|
||||
interface DetoxIosAppConfig {
|
||||
@@ -216,7 +261,7 @@ declare global {
|
||||
type: 'android.emulator';
|
||||
device: string | { avdName: string };
|
||||
bootArgs?: string;
|
||||
gpuMode?: 'auto' | 'host' | 'swiftshader_indirect' | 'angle_indirect' | 'guest';
|
||||
gpuMode?: 'auto' | 'host' | 'swiftshader_indirect' | 'angle_indirect' | 'guest' | 'off';
|
||||
headless?: boolean;
|
||||
/**
|
||||
* @default true
|
||||
@@ -265,51 +310,111 @@ declare global {
|
||||
|
||||
// endregion DetoxConfig
|
||||
|
||||
// Detox exports all methods from detox global and all of the global constants.
|
||||
interface DetoxInstance {
|
||||
device: Device;
|
||||
element: ElementFacade;
|
||||
waitFor: WaitForFacade;
|
||||
expect: ExpectFacade;
|
||||
by: ByFacade;
|
||||
web: WebFacade;
|
||||
interface DetoxExportWrapper {
|
||||
readonly device: Device;
|
||||
|
||||
readonly element: ElementFacade;
|
||||
|
||||
readonly waitFor: WaitForFacade;
|
||||
|
||||
readonly expect: ExpectFacade;
|
||||
|
||||
readonly by: ByFacade;
|
||||
|
||||
readonly web: WebFacade;
|
||||
|
||||
readonly DetoxConstants: {
|
||||
userNotificationTriggers: {
|
||||
push: 'push';
|
||||
calendar: 'calendar';
|
||||
timeInterval: 'timeInterval';
|
||||
location: 'location';
|
||||
};
|
||||
userActivityTypes: {
|
||||
searchableItem: string;
|
||||
browsingWeb: string;
|
||||
},
|
||||
searchableItemActivityIdentifier: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detox logger instance. Can be used for saving user logs to the general log file.
|
||||
*/
|
||||
readonly log: Logger;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* Deprecated - use {@link Detox.Logger#trace}
|
||||
* Detox tracer instance. Can be used for building timelines in Google Event Tracing format.
|
||||
*/
|
||||
readonly trace: {
|
||||
/** @deprecated */
|
||||
readonly startSection: (name: string) => void;
|
||||
/** @deprecated */
|
||||
readonly endSection: (name: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
readonly traceCall: <T>(event: string, action: () => Promise<T>) => Promise<T>;
|
||||
}
|
||||
|
||||
interface DetoxExportWrapper extends DetoxInstance {
|
||||
/**
|
||||
* The setup phase happens inside detox.init(). This is the phase where detox reads its configuration, starts a server, loads its expection library and starts a simulator
|
||||
*
|
||||
* @param configOverride - this object is deep-merged with the selected Detox configuration from .detoxrc
|
||||
* @example
|
||||
* beforeAll(async () => {
|
||||
* await detox.init();
|
||||
* });
|
||||
*/
|
||||
init(configOverride?: Partial<DetoxConfig>): Promise<void>;
|
||||
interface Logger {
|
||||
readonly level: DetoxLogLevel;
|
||||
|
||||
beforeEach(...args: any[]): Promise<void>;
|
||||
readonly fatal: _LogMethod;
|
||||
readonly error: _LogMethod;
|
||||
readonly warn: _LogMethod;
|
||||
readonly info: _LogMethod;
|
||||
readonly debug: _LogMethod;
|
||||
readonly trace: _LogMethod;
|
||||
|
||||
afterEach(...args: any[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* The cleanup phase should happen after all the tests have finished.
|
||||
* This is the phase where the Detox server shuts down.
|
||||
*
|
||||
* @example
|
||||
* after(async () => {
|
||||
* await detox.cleanup();
|
||||
* });
|
||||
*/
|
||||
cleanup(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Unstable. API to access an assembled detox config before it gets passed to testRunner
|
||||
* or detox.init(). Use it only if you don't have another option.
|
||||
* @internal
|
||||
*/
|
||||
hook(event: 'UNSAFE_configReady', listener: (config: unknown) => void): void;
|
||||
child(context?: Partial<LogEvent>): Logger;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
interface _LogMethod extends _LogMethodSignature {
|
||||
readonly begin: _LogMethodSignature;
|
||||
readonly complete: _CompleteMethodSignature;
|
||||
readonly end: _LogMethodSignature;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
interface _LogMethodSignature {
|
||||
(...args: unknown[]): void
|
||||
(event: LogEvent, ...args: unknown[]): void;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
interface _CompleteMethodSignature {
|
||||
<T>(message: string, action: T | (() => T)): T;
|
||||
<T>(event: LogEvent, message: string, action: T | (() => T)): T;
|
||||
}
|
||||
|
||||
type LogEvent = {
|
||||
/** Use when there's a risk of logging several parallel duration events. */
|
||||
id?: string | number;
|
||||
/** Optional. Event categories (tags) to facilitate filtering. */
|
||||
cat?: string | string[];
|
||||
/** Optional. Color name (applicable in Google Chrome Trace Format) */
|
||||
cname?: string;
|
||||
|
||||
/** Reserved property. Process ID. */
|
||||
pid?: never;
|
||||
/** Reserved property. Thread ID. */
|
||||
tid?: never;
|
||||
/** Reserved property. Timestamp. */
|
||||
ts?: never;
|
||||
/** Reserved property. Event phase. */
|
||||
ph?: never;
|
||||
|
||||
[customProperty: string]: unknown;
|
||||
};
|
||||
|
||||
type DetoxLogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
||||
|
||||
type Point2D = {
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -396,12 +501,10 @@ declare global {
|
||||
* Holds the environment-unique ID of the device - namely, the adb ID on Android (e.g. emulator-5554) and the Mac-global simulator UDID on iOS,
|
||||
* as used by simctl (e.g. AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE).
|
||||
*
|
||||
* The value will be undefined until the device is properly prepared (i.e. in detox.init())
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Holds a descriptive name of the device. Example: emulator-5554 (Pixel_API_29)
|
||||
* The value will be undefined until the device is properly prepared (i.e. in detox.init()).
|
||||
*/
|
||||
name: string;
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('./src/realms');
|
||||
Vendored
+184
@@ -0,0 +1,184 @@
|
||||
/// <reference types="node" />
|
||||
/// <reference path="index.d.ts"/>
|
||||
|
||||
declare global {
|
||||
namespace DetoxInternals {
|
||||
type DetoxStatus = 'inactive' | 'init' | 'active' | 'cleanup';
|
||||
|
||||
type Facade = {
|
||||
// region Initialization
|
||||
/**
|
||||
* Use with a caution, when you still have no config, yet need to avoid {@link Facade#init}
|
||||
*/
|
||||
resolveConfig(options?: Partial<DetoxInitOptions>): Promise<RuntimeConfig>;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
getStatus(): DetoxStatus;
|
||||
|
||||
/**
|
||||
* This is the phase where Detox reads its configuration, starts a server.
|
||||
*/
|
||||
init(options?: Partial<DetoxInitOptions>): Promise<void>;
|
||||
|
||||
/**
|
||||
* This is the phase where Detox loads its expectation library and starts a device.
|
||||
*/
|
||||
installWorker(options?: Partial<DetoxInstallWorkerOptions>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Deallocates the device.
|
||||
*/
|
||||
uninstallWorker(): Promise<void>;
|
||||
|
||||
/**
|
||||
* The global cleanup phase should happen after all the tests have finished.
|
||||
* This is the phase where the Detox server shuts down.
|
||||
*/
|
||||
cleanup(): Promise<void>;
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
onRunStart(event: unknown): Promise<void>;
|
||||
onRunDescribeStart(event: unknown): Promise<void>;
|
||||
onTestStart(event: unknown): Promise<void>;
|
||||
onHookStart(event: unknown): Promise<void>;
|
||||
onHookFailure(event: unknown): Promise<void>;
|
||||
onHookSuccess(event: unknown): Promise<void>;
|
||||
onTestFnStart(event: unknown): Promise<void>;
|
||||
onTestFnFailure(event: unknown): Promise<void>;
|
||||
onTestFnSuccess(event: unknown): Promise<void>;
|
||||
onTestDone(event: unknown): Promise<void>;
|
||||
onRunDescribeFinish(event: unknown): Promise<void>;
|
||||
onRunFinish(event: unknown): Promise<void>;
|
||||
|
||||
/**
|
||||
* Reports to Detox CLI about failed tests that could have been re-run if
|
||||
* {@link Detox.DetoxTestRunnerConfig#retries} is set to a non-zero.
|
||||
*
|
||||
* @param testFilePaths array of failed test files' paths
|
||||
* @param permanent whether the failure is permanent, and the tests
|
||||
* should not be re-run.
|
||||
*/
|
||||
reportFailedTests(testFilePaths: string[], permanent?: boolean): Promise<void>;
|
||||
// endregion
|
||||
|
||||
readonly config: RuntimeConfig;
|
||||
readonly log: Detox.Logger;
|
||||
readonly session: SessionState;
|
||||
|
||||
readonly worker: Worker;
|
||||
}
|
||||
|
||||
interface Worker extends Detox.DetoxExportWrapper {
|
||||
readonly id: string;
|
||||
}
|
||||
|
||||
type DetoxInitOptions = {
|
||||
cwd: string;
|
||||
argv: Record<string, unknown>;
|
||||
testRunnerArgv: Record<string, unknown>;
|
||||
override: Partial<Detox.DetoxConfig>;
|
||||
/** @inheritDoc */
|
||||
global: NodeJS.Global;
|
||||
/**
|
||||
* Worker ID. Used to distinguish allocated workers in parallel test execution environment.
|
||||
*
|
||||
* If explicitly set to null, tells {@link Facade#init} to skip {@link Facade#installWorker} call.
|
||||
* Useful for complex test runner integrations, where you have to install the worker via a separate call,
|
||||
* when the environment is ready for that.
|
||||
*
|
||||
* @default 'worker'
|
||||
*/
|
||||
workerId: string | null;
|
||||
};
|
||||
|
||||
type DetoxInstallWorkerOptions = {
|
||||
/**
|
||||
* Used for integration with sandboxed test environments.
|
||||
* {@link DetoxInternals.Facade#setup} might override {@link Console} methods
|
||||
* to integrate it with Detox loggeing subsystem.
|
||||
*/
|
||||
global: NodeJS.Global;
|
||||
/**
|
||||
* Worker ID. Used to distinguish allocated workers in parallel test execution environment.
|
||||
*
|
||||
* @default 'worker'
|
||||
*/
|
||||
workerId: string;
|
||||
};
|
||||
|
||||
type SessionState = Readonly<{
|
||||
/**
|
||||
* Randomly generated ID for the entire Detox test session, including retries.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Permanently failed test file paths.
|
||||
*/
|
||||
failedTestFiles: string[];
|
||||
/**
|
||||
* Failed test file paths suggested to retry via Detox CLI mechanism.
|
||||
*/
|
||||
testFilesToRetry: string[];
|
||||
/**
|
||||
* Retry index of the test session: 0..retriesCount.
|
||||
*/
|
||||
testSessionIndex: number;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
workersCount: number;
|
||||
}>;
|
||||
|
||||
type RuntimeConfig = Readonly<{
|
||||
configurationName: string;
|
||||
|
||||
/**
|
||||
* Dictionary of app configurations,
|
||||
* where the keys are defined by {@link Detox.DetoxAppConfig#name}
|
||||
* or equal to "default" if the name is not configured.
|
||||
*/
|
||||
apps: Record<string, Readonly<Detox.DetoxAppConfig>>;
|
||||
artifacts: Readonly<Detox.DetoxArtifactsConfig>;
|
||||
behavior: Readonly<Detox.DetoxBehaviorConfig>;
|
||||
cli: Readonly<CLIConfig>;
|
||||
device: Readonly<Detox.DetoxDeviceConfig>;
|
||||
logger: Readonly<Detox.DetoxLoggerConfig>;
|
||||
testRunner: Readonly<Detox.DetoxTestRunnerConfig>;
|
||||
session: Readonly<Detox.DetoxSessionConfig>;
|
||||
}>;
|
||||
|
||||
type CLIConfig = Readonly<Partial<{
|
||||
appLaunchArgs: string;
|
||||
artifactsLocation: string;
|
||||
captureViewHierarchy: string;
|
||||
cleanup: boolean;
|
||||
configPath: string;
|
||||
configuration: string;
|
||||
debugSynchronization: number;
|
||||
deviceBootArgs: string;
|
||||
deviceName: string;
|
||||
forceAdbInstall: boolean;
|
||||
gpu: string;
|
||||
inspectBrk: boolean;
|
||||
headless: boolean;
|
||||
jestReportSpecs: boolean;
|
||||
keepLockFile: boolean;
|
||||
loglevel: string;
|
||||
readonlyEmu: boolean;
|
||||
recordLogs: string;
|
||||
recordPerformance: string;
|
||||
recordVideos: string;
|
||||
retries: number;
|
||||
reuse: string;
|
||||
takeScreenshots: string;
|
||||
useCustomLogger: string;
|
||||
}>>;
|
||||
}
|
||||
}
|
||||
|
||||
declare const detox: DetoxInternals.Facade;
|
||||
export = detox;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const realm = require('./src/realms');
|
||||
const DetoxInternalsFacade = require('./src/realms/DetoxInternalsFacade');
|
||||
|
||||
module.exports = new DetoxInternalsFacade(realm);
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const DETOX_REGEXP = /^DETOX_(?!DISABLE_POD_INSTALL|DISABLE_POSTINSTALL)/;
|
||||
|
||||
const env = Object.keys(process.env)
|
||||
.filter(k => DETOX_REGEXP.test(k.toUpperCase()))
|
||||
.reduce((acc, key) => {
|
||||
acc[key] = process.env[key];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const [, ...argv] = process.argv;
|
||||
|
||||
const output = JSON.stringify({ env, argv }) + '\n';
|
||||
if (process.env.CLI_TEST_STDOUT) {
|
||||
fs.appendFileSync(process.env.CLI_TEST_STDOUT, output);
|
||||
} else {
|
||||
process.stdout.write(output);
|
||||
}
|
||||
|
||||
if (process.env.CLI_EXIT_CODE) {
|
||||
process.exit(+process.env.CLI_EXIT_CODE);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ const cp = require('child_process');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const log = require('../src/utils/logger').child({ __filename });
|
||||
const detox = require('../internals');
|
||||
|
||||
module.exports.command = 'build-framework-cache';
|
||||
module.exports.desc = 'Builds a cached Detox framework for the current environment in ~/Library/Detox. The cached framework is unique for each combination of Xcode and Detox version. (macOS only)';
|
||||
@@ -11,6 +11,6 @@ module.exports.handler = async function buildFrameworkCache() {
|
||||
if (os.platform() === 'darwin') {
|
||||
cp.execSync(path.join(__dirname, '../scripts/build_framework.ios.sh'), { stdio: 'inherit' });
|
||||
} else {
|
||||
log.info(`The command is supported only on macOS, skipping the execution.`);
|
||||
detox.log.info(`The command is supported only on macOS, skipping the execution.`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,8 +4,7 @@ const fs = require('fs');
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const { composeDetoxConfig } = require('../src/configuration');
|
||||
const log = require('../src/utils/logger').child({ __filename });
|
||||
const detox = require('../internals');
|
||||
|
||||
module.exports.command = 'build';
|
||||
module.exports.desc = "Runs the user-provided build command, as defined in the 'build' property of the specified configuration.";
|
||||
@@ -38,27 +37,27 @@ module.exports.builder = {
|
||||
};
|
||||
|
||||
module.exports.handler = async function build(argv) {
|
||||
const { errorComposer, appsConfig } = await composeDetoxConfig({ argv });
|
||||
const { apps: appsConfig, errorComposer } = await detox.resolveConfig({ argv });
|
||||
const apps = _.entries(appsConfig);
|
||||
|
||||
for (const [appName, app] of apps) {
|
||||
const buildScript = app.build;
|
||||
|
||||
if (argv['if-missing'] && app.binaryPath && fs.existsSync(app.binaryPath)) {
|
||||
log.info(`Skipping build for "${appName}" app...`);
|
||||
detox.log.info(`Skipping build for "${appName}" app...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (buildScript) {
|
||||
try {
|
||||
if (apps.length > 1) {
|
||||
log.info(`Building "${appName}" app...`);
|
||||
detox.log.info(`Building "${appName}" app...`);
|
||||
}
|
||||
|
||||
log.info(buildScript);
|
||||
detox.log.info(buildScript);
|
||||
cp.execSync(buildScript, { stdio: 'inherit' });
|
||||
} catch (e) {
|
||||
log.warn("\n\nImportant: 'detox build' is a convenience shortcut for calling your own build command, as provided in the config file.\nFailures in this build command are not the responsibility of Detox. You are responsible for maintaining this command.\n");
|
||||
detox.log.warn("\n\nImportant: 'detox build' is a convenience shortcut for calling your own build command, as provided in the config file.\nFailures in this build command are not the responsibility of Detox. You are responsible for maintaining this command.\n");
|
||||
throw e;
|
||||
}
|
||||
} else if (!argv.silent) {
|
||||
@@ -66,7 +65,7 @@ module.exports.handler = async function build(argv) {
|
||||
}
|
||||
|
||||
if (app.binaryPath && !fs.existsSync(app.binaryPath)) {
|
||||
log.warn('\nImportant: after running the build command, Detox could not find your app at the given binary path:\n\t' + app.binaryPath + "\nMake sure it is correct, otherwise you'll get an error on an attempt to install your app.\n");
|
||||
detox.log.warn('\nImportant: after running the build command, Detox could not find your app at the given binary path:\n\t' + app.binaryPath + "\nMake sure it is correct, otherwise you'll get an error on an attempt to install your app.\n");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,51 +1,57 @@
|
||||
// @ts-nocheck
|
||||
jest.mock('child_process');
|
||||
jest.mock('../src/utils/logger');
|
||||
jest.mock('../src/configuration');
|
||||
|
||||
const tempfile = require('tempfile');
|
||||
|
||||
const DetoxConfigErrorComposer = require('../src/errors/DetoxConfigErrorComposer');
|
||||
|
||||
describe('build', () => {
|
||||
let log, execSync, composeDetoxConfig, detoxConfig;
|
||||
let execSync, detox, callCli;
|
||||
|
||||
beforeEach(() => {
|
||||
detoxConfig = {
|
||||
appsConfig: {},
|
||||
artifactsConfig: {},
|
||||
behaviorConfig: {},
|
||||
deviceConfig: {},
|
||||
sessionConfig: {},
|
||||
errorComposer: new DetoxConfigErrorComposer(),
|
||||
};
|
||||
|
||||
log = require('../src/utils/logger');
|
||||
// @ts-ignore
|
||||
callCli = global.callCli;
|
||||
jest.mock('child_process');
|
||||
execSync = require('child_process').execSync;
|
||||
composeDetoxConfig = require('../src/configuration').composeDetoxConfig;
|
||||
composeDetoxConfig.mockReturnValue(Promise.resolve(detoxConfig));
|
||||
|
||||
jest.mock('../src/utils/logger');
|
||||
jest.mock('../internals', () => {
|
||||
const DetoxConfigErrorComposer = require('../src/errors/DetoxConfigErrorComposer');
|
||||
|
||||
const config = {
|
||||
apps: {},
|
||||
artifacts: {},
|
||||
behavior: {},
|
||||
errorComposer: new DetoxConfigErrorComposer(),
|
||||
device: {},
|
||||
session: {}
|
||||
};
|
||||
|
||||
return ({
|
||||
config,
|
||||
resolveConfig: jest.fn().mockResolvedValue(config),
|
||||
log: require('../src/utils/logger')
|
||||
});
|
||||
});
|
||||
|
||||
detox = require('../internals');
|
||||
});
|
||||
|
||||
it('passes argv to composeConfig', async () => {
|
||||
it('passes argv to resolveConfig', async () => {
|
||||
await callCli('./build', 'build -C /etc/.detoxrc.js -c myconf').catch(() => {});
|
||||
|
||||
expect(composeDetoxConfig).toHaveBeenCalledWith({
|
||||
expect(detox.resolveConfig).toHaveBeenCalledWith({
|
||||
argv: expect.objectContaining({
|
||||
'config-path': '/etc/.detoxrc.js',
|
||||
'configuration': 'myconf',
|
||||
'C': '/etc/.detoxrc.js',
|
||||
'c': 'myconf',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('runs the build script from the composed device config', async () => {
|
||||
detoxConfig.appsConfig.default = { build: 'yet another command' };
|
||||
detox.config.apps.default = { build: 'yet another command' };
|
||||
|
||||
await callCli('./build', 'build');
|
||||
expect(execSync).toHaveBeenCalledWith('yet another command', expect.anything());
|
||||
});
|
||||
|
||||
it('skips building the app if the binary exists and --if-missing flag is set', async () => {
|
||||
detoxConfig.appsConfig.default = { build: 'yet another command', binaryPath: __filename };
|
||||
detox.config.apps.default = { build: 'yet another command', binaryPath: __filename };
|
||||
|
||||
await callCli('./build', 'build -i');
|
||||
expect(execSync).not.toHaveBeenCalled();
|
||||
@@ -53,46 +59,46 @@ describe('build', () => {
|
||||
await callCli('./build', 'build --if-missing');
|
||||
expect(execSync).not.toHaveBeenCalled();
|
||||
|
||||
expect(log.info).toHaveBeenCalledWith('Skipping build for "default" app...');
|
||||
expect(detox.log.info).toHaveBeenCalledWith('Skipping build for "default" app...');
|
||||
});
|
||||
|
||||
it('fails with an error if a build script has not been found', async () => {
|
||||
detoxConfig.appsConfig.default = {};
|
||||
detox.config.apps.default = {};
|
||||
await expect(callCli('./build', 'build')).rejects.toThrowError(/Failed to build/);
|
||||
});
|
||||
|
||||
it('should ignore missing build command with -s, --silent flag', async () => {
|
||||
detoxConfig.appsConfig.default = {};
|
||||
detox.config.apps.default = {};
|
||||
await expect(callCli('./build', 'build -s')).resolves.not.toThrowError();
|
||||
expect(log.warn).not.toHaveBeenCalled();
|
||||
expect(detox.log.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should print a warning upon user build script failure', async () => {
|
||||
detoxConfig.appsConfig.default = { build: 'a command' };
|
||||
detox.config.apps.default = { build: 'a command' };
|
||||
execSync.mockImplementation(() => { throw new Error('Build failure'); });
|
||||
await expect(callCli('./build', 'build')).rejects.toThrowError(/Build failure/);
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('You are responsible'));
|
||||
expect(detox.log.warn).toHaveBeenCalledWith(expect.stringContaining('You are responsible'));
|
||||
});
|
||||
|
||||
it('should print a warning if app is not found at binary path', async () => {
|
||||
detoxConfig.appsConfig.default = { binaryPath: tempfile() };
|
||||
detox.config.apps.default = { binaryPath: tempfile() };
|
||||
await expect(callCli('./build', 'build -s')).resolves.not.toThrowError();
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('could not find your app at the given binary path'));
|
||||
expect(detox.log.warn).toHaveBeenCalledWith(expect.stringContaining('could not find your app at the given binary path'));
|
||||
});
|
||||
|
||||
it('should print extra message with the app name before building (in a multi-app configuration)', async () => {
|
||||
detoxConfig.appsConfig.app1 = { binaryPath: tempfile(), build: ':' };
|
||||
detoxConfig.appsConfig.app2 = { binaryPath: tempfile(), build: ':' };
|
||||
detox.config.apps.app1 = { binaryPath: tempfile(), build: ':' };
|
||||
detox.config.apps.app2 = { binaryPath: tempfile(), build: ':' };
|
||||
|
||||
await expect(callCli('./build', 'build -s')).resolves.not.toThrowError();
|
||||
expect(log.info).toHaveBeenCalledWith(expect.stringContaining('app1'));
|
||||
expect(log.info).toHaveBeenCalledWith(expect.stringContaining('app2'));
|
||||
expect(detox.log.info).toHaveBeenCalledWith(expect.stringContaining('app1'));
|
||||
expect(detox.log.info).toHaveBeenCalledWith(expect.stringContaining('app2'));
|
||||
});
|
||||
|
||||
it('should not print that extra message when the app is single', async () => {
|
||||
detoxConfig.appsConfig.default = { binaryPath: tempfile(), build: ':' };
|
||||
detox.config.apps.default = { binaryPath: tempfile(), build: ':' };
|
||||
|
||||
await expect(callCli('./build', 'build -s')).resolves.not.toThrowError();
|
||||
expect(log.info).not.toHaveBeenCalledWith(expect.stringContaining('default'));
|
||||
expect(detox.log.info).not.toHaveBeenCalledWith(expect.stringContaining('default'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ const path = require('path');
|
||||
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const log = require('../src/utils/logger').child({ __filename });
|
||||
const detox = require('../internals');
|
||||
|
||||
module.exports.command = 'clean-framework-cache';
|
||||
module.exports.desc = "Deletes all Detox cached frameworks from ~/Library/Detox. Cached framework can be rebuilt using the 'build-framework-cache' command. (macOS only)";
|
||||
@@ -11,9 +11,9 @@ module.exports.desc = "Deletes all Detox cached frameworks from ~/Library/Detox.
|
||||
module.exports.handler = async function cleanFrameworkCache() {
|
||||
if (os.platform() === 'darwin') {
|
||||
const frameworkPath = path.join(os.homedir(), '/Library/Detox');
|
||||
log.info(`Removing framework binaries from ${frameworkPath}`);
|
||||
detox.log.info(`Removing framework binaries from ${frameworkPath}`);
|
||||
await fs.remove(frameworkPath);
|
||||
} else {
|
||||
log.info(`The command is supported only on macOS, skipping the execution.`);
|
||||
detox.log.info(`The command is supported only on macOS, skipping the execution.`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
global.DETOX_CLI = true;
|
||||
const yargs = require('yargs');
|
||||
|
||||
const DetoxError = require('../src/errors/DetoxError');
|
||||
const logger = require('../src/utils/logger').child({ __filename });
|
||||
const logger = require('../src/utils/logger').child({ cat: 'cli' });
|
||||
|
||||
yargs
|
||||
.scriptName('detox')
|
||||
|
||||
+16
-7
@@ -1,7 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const log = require('../src/utils/logger').child({ __filename });
|
||||
const detox = require('../internals');
|
||||
|
||||
const jestTemplates = require('./templates/jest');
|
||||
|
||||
@@ -43,7 +43,7 @@ function createFile(filename, content) {
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filename, content);
|
||||
log.info(`Created a file at path: ${filename}`);
|
||||
detox.log.info(`Created a file at path: ${filename}`);
|
||||
} catch (err) {
|
||||
reportError({ err }, `Failed to create a file at path: ${filename}`);
|
||||
}
|
||||
@@ -52,19 +52,28 @@ function createFile(filename, content) {
|
||||
function createJestFolderE2E() {
|
||||
createFolder('e2e', {
|
||||
'config.json': jestTemplates.runnerConfig,
|
||||
'environment.js': jestTemplates.environment,
|
||||
'firstTest.e2e.js': jestTemplates.firstTest,
|
||||
'starter.test.js': jestTemplates.starter,
|
||||
});
|
||||
|
||||
createFile('.detoxrc.json', JSON.stringify({
|
||||
testRunner: 'jest',
|
||||
runnerConfig: 'e2e/config.json',
|
||||
testRunner: {
|
||||
args: {
|
||||
config: 'e2e/config.json'
|
||||
},
|
||||
},
|
||||
|
||||
...createDefaultConfigurations(),
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
function createDefaultConfigurations() {
|
||||
return {
|
||||
testRunner: {
|
||||
args: {
|
||||
$0: 'jest',
|
||||
config: 'e2e/config.json',
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
ios: {
|
||||
type: 'ios.app',
|
||||
@@ -103,6 +112,6 @@ function createDefaultConfigurations() {
|
||||
}
|
||||
|
||||
function reportError(...args) {
|
||||
log.error(...args);
|
||||
detox.log.error(...args);
|
||||
exitCode = 1;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const path = require('path');
|
||||
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const log = require('../src/utils/logger').child({ __filename });
|
||||
const log = require('../src/utils/logger').child({ cat: 'cli' });
|
||||
|
||||
module.exports.command = 'rebuild-framework-cache';
|
||||
module.exports.desc = 'Rebuilds a cached Detox framework for the current environment in ~/Library/Detox. The cached framework is unique for each combination of Xcode and Detox version. (macOS only)';
|
||||
|
||||
@@ -1,40 +1,16 @@
|
||||
const firstTestContent = require('./firstTestContent');
|
||||
|
||||
const runnerConfig = `{
|
||||
"maxWorkers": 1,
|
||||
"testEnvironment": "./environment",
|
||||
"testTimeout": 120000,
|
||||
"testMatch": ["<rootDir>/e2e/*.e2e.js"],
|
||||
"reporters": ["detox/runners/jest/reporter"],
|
||||
"verbose": true
|
||||
"globalSetup": "detox/runners/jest/globalSetup",
|
||||
"globalTeardown": "detox/runners/jest/globalTeardown",
|
||||
"maxWorkers": 1,
|
||||
"reporters": ["detox/runners/jest/reporter"],
|
||||
"testEnvironment": "detox/runners/jest/testEnvironment",
|
||||
"testMatch": ["<rootDir>/e2e/**/*.test.js"],
|
||||
"testTimeout": 120000,
|
||||
"verbose": true
|
||||
}
|
||||
`;
|
||||
|
||||
const environmentJsContent = `const {
|
||||
DetoxCircusEnvironment,
|
||||
SpecReporter,
|
||||
WorkerAssignReporter,
|
||||
} = require('detox/runners/jest');
|
||||
|
||||
class CustomDetoxEnvironment extends DetoxCircusEnvironment {
|
||||
constructor(config, context) {
|
||||
super(config, context);
|
||||
|
||||
// Can be safely removed, if you are content with the default value (=300000ms)
|
||||
this.initTimeout = 300000;
|
||||
|
||||
// This takes care of generating status logs on a per-spec basis. By default, Jest only reports at file-level.
|
||||
// This is strictly optional.
|
||||
this.registerListeners({
|
||||
SpecReporter,
|
||||
WorkerAssignReporter,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CustomDetoxEnvironment;
|
||||
`;
|
||||
|
||||
exports.environment = environmentJsContent;
|
||||
exports.firstTest = firstTestContent;
|
||||
exports.starter = firstTestContent;
|
||||
exports.runnerConfig = runnerConfig;
|
||||
|
||||
+18
-187
@@ -1,196 +1,27 @@
|
||||
// @ts-nocheck
|
||||
const cp = require('child_process');
|
||||
const detox = require('../internals');
|
||||
|
||||
const _ = require('lodash');
|
||||
const whichSync = require('which').sync;
|
||||
const unparse = require('yargs-unparser');
|
||||
|
||||
const { composeDetoxConfig } = require('../src/configuration');
|
||||
const DeviceRegistry = require('../src/devices/DeviceRegistry');
|
||||
const GenyDeviceRegistryFactory = require('../src/devices/allocation/drivers/android/genycloud/GenyDeviceRegistryFactory');
|
||||
const { loadLastFailedTests, resetLastFailedTests } = require('../src/utils/lastFailedTests');
|
||||
const log = require('../src/utils/logger').child({ __filename });
|
||||
const { parse, quote } = require('../src/utils/shellQuote');
|
||||
|
||||
const { readJestConfig } = require('./utils/jestInternals');
|
||||
const { getPlatformSpecificString, printEnvironmentVariables } = require('./utils/misc');
|
||||
const { prependNodeModulesBinToPATH } = require('./utils/misc');
|
||||
const splitArgv = require('./utils/splitArgv');
|
||||
const { DETOX_ARGV_OVERRIDE_NOTICE, DEVICE_LAUNCH_ARGS_DEPRECATION } = require('./utils/warnings');
|
||||
const TestRunnerCommand = require('./testCommand/TestRunnerCommand');
|
||||
|
||||
module.exports.command = 'test';
|
||||
module.exports.desc = 'Run your test suite with the test runner specified in package.json';
|
||||
module.exports.builder = require('./utils/testCommandArgs');
|
||||
module.exports.handler = async function test(argv) {
|
||||
const { detoxArgs, runnerArgs } = splitArgv.detox(argv);
|
||||
const { cliConfig, deviceConfig, runnerConfig } = await composeDetoxConfig({ argv: detoxArgs });
|
||||
const [platform] = deviceConfig.type.split('.');
|
||||
module.exports.builder = require('./testCommand/builder');
|
||||
module.exports.middlewares = require('./testCommand/middlewares').default;
|
||||
|
||||
const forwardedArgs = await prepareArgs({
|
||||
cliConfig,
|
||||
deviceConfig,
|
||||
runnerConfig,
|
||||
runnerArgs,
|
||||
platform,
|
||||
});
|
||||
|
||||
if (detoxArgs['inspect-brk']) {
|
||||
const runnerBinary = whichSync('jest', {
|
||||
path: prependNodeModulesBinToPATH({ ...process.env }),
|
||||
module.exports.handler = async function test({ detoxArgs, runnerArgs }) {
|
||||
try {
|
||||
await detox.init({
|
||||
argv: detoxArgs,
|
||||
testRunnerArgv: runnerArgs,
|
||||
workerId: null,
|
||||
});
|
||||
|
||||
forwardedArgs.argv.$0 = `node --inspect-brk ${require.resolve(runnerBinary)}`;
|
||||
} else {
|
||||
forwardedArgs.argv.$0 = runnerConfig.testRunner;
|
||||
}
|
||||
const runnerCommand = new TestRunnerCommand()
|
||||
.setRunnerConfig(detox.config.testRunner)
|
||||
.setDeviceConfig(detox.config.device)
|
||||
.replicateCLIConfig(detox.config.cli);
|
||||
|
||||
await runTestRunnerWithRetries(forwardedArgs, {
|
||||
keepLockFile: cliConfig.keepLockFile,
|
||||
retries: detoxArgs.retries,
|
||||
platform,
|
||||
});
|
||||
await runnerCommand.execute();
|
||||
} finally {
|
||||
await detox.cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.middlewares = [
|
||||
function applyEnvironmentVariableAddendum(argv, yargs) {
|
||||
if (process.env.DETOX_ARGV_OVERRIDE) {
|
||||
log.warn(DETOX_ARGV_OVERRIDE_NOTICE);
|
||||
|
||||
return yargs.parse([
|
||||
...process.argv.slice(2),
|
||||
...parse(process.env.DETOX_ARGV_OVERRIDE),
|
||||
]);
|
||||
}
|
||||
|
||||
return argv;
|
||||
},
|
||||
|
||||
function warnDeviceAppLaunchArgsDeprecation(argv) {
|
||||
if (argv['device-boot-args'] && process.argv.some(a => a.startsWith('--device-launch-args'))) {
|
||||
log.warn(DEVICE_LAUNCH_ARGS_DEPRECATION);
|
||||
}
|
||||
|
||||
return argv;
|
||||
}
|
||||
];
|
||||
|
||||
async function prepareArgs({ cliConfig, deviceConfig, runnerArgs, runnerConfig, platform }) {
|
||||
const { specs, passthrough } = splitArgv.jest(runnerArgs);
|
||||
const platformFilter = getPlatformSpecificString(platform);
|
||||
|
||||
const argv = _.omitBy({
|
||||
color: !cliConfig.noColor && undefined,
|
||||
config: runnerConfig.runnerConfig /* istanbul ignore next */ || undefined,
|
||||
testNamePattern: platformFilter ? `^((?!${platformFilter}).)*$` : undefined,
|
||||
maxWorkers: cliConfig.workers || undefined,
|
||||
|
||||
...passthrough,
|
||||
}, _.isUndefined);
|
||||
|
||||
const hasMultipleWorkers = (await readJestConfig(argv)).globalConfig.maxWorkers > 1;
|
||||
|
||||
return {
|
||||
argv,
|
||||
|
||||
env: _.omitBy({
|
||||
DETOX_APP_LAUNCH_ARGS: cliConfig.appLaunchArgs,
|
||||
DETOX_ARTIFACTS_LOCATION: cliConfig.artifactsLocation,
|
||||
DETOX_CAPTURE_VIEW_HIERARCHY: cliConfig.captureViewHierarchy,
|
||||
DETOX_CLEANUP: cliConfig.cleanup,
|
||||
DETOX_CONFIGURATION: cliConfig.configuration,
|
||||
DETOX_CONFIG_PATH: cliConfig.configPath,
|
||||
DETOX_DEBUG_SYNCHRONIZATION: cliConfig.debugSynchronization,
|
||||
DETOX_DEVICE_BOOT_ARGS: cliConfig.deviceBootArgs,
|
||||
DETOX_DEVICE_NAME: cliConfig.deviceName,
|
||||
DETOX_FORCE_ADB_INSTALL: platform === 'android' ? cliConfig.forceAdbInstall : undefined,
|
||||
DETOX_GPU: cliConfig.gpu,
|
||||
DETOX_HEADLESS: cliConfig.headless,
|
||||
DETOX_LOGLEVEL: cliConfig.loglevel,
|
||||
DETOX_READ_ONLY_EMU: deviceConfig.type === 'android.emulator' && hasMultipleWorkers ? true : undefined,
|
||||
DETOX_RECORD_LOGS: cliConfig.recordLogs,
|
||||
DETOX_RECORD_PERFORMANCE: cliConfig.recordPerformance,
|
||||
DETOX_RECORD_TIMELINE: cliConfig.recordTimeline,
|
||||
DETOX_RECORD_VIDEOS: cliConfig.recordVideos,
|
||||
DETOX_REPORT_SPECS: _.isUndefined(cliConfig.jestReportSpecs)
|
||||
? !hasMultipleWorkers
|
||||
: `${cliConfig.jestReportSpecs}` === 'true',
|
||||
DETOX_REUSE: cliConfig.reuse,
|
||||
DETOX_START_TIMESTAMP: Date.now(),
|
||||
DETOX_TAKE_SCREENSHOTS: cliConfig.takeScreenshots,
|
||||
DETOX_USE_CUSTOM_LOGGER: cliConfig.useCustomLogger,
|
||||
}, _.isUndefined),
|
||||
|
||||
specs: _.isEmpty(specs) && runnerConfig.specs ? [runnerConfig.specs] : specs,
|
||||
};
|
||||
}
|
||||
|
||||
async function resetLockFile({ platform }) {
|
||||
if (platform === 'ios') {
|
||||
await DeviceRegistry.forIOS().reset();
|
||||
}
|
||||
|
||||
if (platform === 'android') {
|
||||
await DeviceRegistry.forAndroid().reset();
|
||||
await GenyDeviceRegistryFactory.forGlobalShutdown().reset();
|
||||
}
|
||||
}
|
||||
|
||||
function launchTestRunner({ argv, env, specs }) {
|
||||
const { $0: command, ...restArgv } = argv;
|
||||
const fullCommand = [
|
||||
command,
|
||||
quote(unparse(_.omitBy(restArgv, _.isUndefined))),
|
||||
specs.join(' ')
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
log.info(printEnvironmentVariables(env) + fullCommand);
|
||||
|
||||
cp.execSync(fullCommand, {
|
||||
stdio: 'inherit',
|
||||
env: _({})
|
||||
.assign(process.env)
|
||||
.assign(env)
|
||||
.omitBy(_.isUndefined)
|
||||
.tap(prependNodeModulesBinToPATH)
|
||||
.value()
|
||||
});
|
||||
}
|
||||
|
||||
async function runTestRunnerWithRetries(forwardedArgs, { keepLockFile, platform, retries }) {
|
||||
let runsLeft = 1 + retries;
|
||||
let launchError;
|
||||
|
||||
do {
|
||||
try {
|
||||
if (launchError) {
|
||||
const list = forwardedArgs.specs.map((file, index) => ` ${index + 1}. ${file}`).join('\n');
|
||||
log.error(
|
||||
`There were failing tests in the following files:\n${list}\n\n` +
|
||||
'Detox CLI is going to restart the test runner with those files...\n'
|
||||
);
|
||||
}
|
||||
|
||||
if (!keepLockFile) {
|
||||
await resetLockFile({ platform });
|
||||
}
|
||||
|
||||
await resetLastFailedTests();
|
||||
launchTestRunner(forwardedArgs);
|
||||
launchError = null;
|
||||
} catch (e) {
|
||||
launchError = e;
|
||||
|
||||
const lastFailedTests = await loadLastFailedTests();
|
||||
if (_.isEmpty(lastFailedTests)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
forwardedArgs.specs = lastFailedTests;
|
||||
forwardedArgs.env.DETOX_RERUN_INDEX = 1 + (forwardedArgs.env.DETOX_RERUN_INDEX || 0);
|
||||
}
|
||||
} while (launchError && --runsLeft > 0);
|
||||
|
||||
if (launchError) {
|
||||
throw launchError;
|
||||
}
|
||||
}
|
||||
|
||||
+343
-442
@@ -3,36 +3,50 @@ if (process.platform === 'win32') {
|
||||
jest.retryTimes(1); // TODO: investigate why it gets stuck for the 1st time on Windows
|
||||
}
|
||||
|
||||
jest.mock('child_process');
|
||||
jest.mock('../src/utils/logger');
|
||||
jest.mock('../src/logger/DetoxLogger');
|
||||
jest.mock('../src/devices/DeviceRegistry');
|
||||
jest.mock('../src/devices/allocation/drivers/android/genycloud/GenyDeviceRegistryFactory');
|
||||
jest.mock('../src/utils/lastFailedTests');
|
||||
jest.mock('./utils/jestInternals');
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const _ = require('lodash');
|
||||
const yargs = require('yargs');
|
||||
|
||||
const { DEVICE_LAUNCH_ARGS_DEPRECATION } = require('./utils/warnings');
|
||||
const { DEVICE_LAUNCH_ARGS_DEPRECATION } = require('./testCommand/warnings');
|
||||
|
||||
describe('CLI', () => {
|
||||
let cp;
|
||||
let _cliCallDump;
|
||||
let _env;
|
||||
let logger;
|
||||
let temporaryFiles;
|
||||
let _temporaryFiles;
|
||||
let detoxConfig;
|
||||
let detoxConfigPath;
|
||||
let DeviceRegistry;
|
||||
let GenyDeviceRegistryFactory;
|
||||
let jestInternals;
|
||||
let _env;
|
||||
|
||||
beforeEach(() => {
|
||||
temporaryFiles = [];
|
||||
_cliCallDump = undefined;
|
||||
_env = process.env;
|
||||
process.env = { ..._env };
|
||||
_temporaryFiles = [];
|
||||
|
||||
process.env = {
|
||||
..._env,
|
||||
CLI_TEST_STDOUT: tempfile('.txt'),
|
||||
};
|
||||
|
||||
const executable = path.relative(process.cwd(), path.join(__dirname, '__mocks__/executable'));
|
||||
|
||||
detoxConfig = {
|
||||
testRunner: {
|
||||
args: {
|
||||
$0: os.platform() === 'win32' ? `node ${executable}` : executable,
|
||||
config: 'e2e/config.json'
|
||||
},
|
||||
},
|
||||
configurations: {
|
||||
single: {
|
||||
device: {
|
||||
@@ -44,8 +58,6 @@ describe('CLI', () => {
|
||||
},
|
||||
};
|
||||
|
||||
cp = require('child_process');
|
||||
|
||||
const realJestInternals = jest.requireActual('./utils/jestInternals');
|
||||
jestInternals = require('./utils/jestInternals');
|
||||
Object.assign(jestInternals, realJestInternals);
|
||||
@@ -61,7 +73,7 @@ describe('CLI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
logger = require('../src/utils/logger');
|
||||
logger = () => require('../src/logger/DetoxLogger').instances[0];
|
||||
DeviceRegistry = require('../src/devices/DeviceRegistry');
|
||||
DeviceRegistry.forAndroid.mockImplementation(() => new DeviceRegistry());
|
||||
DeviceRegistry.forIOS.mockImplementation(() => new DeviceRegistry());
|
||||
@@ -72,7 +84,7 @@ describe('CLI', () => {
|
||||
afterEach(async () => {
|
||||
process.env = _env;
|
||||
|
||||
await Promise.all(temporaryFiles.map(name => fs.remove(name)));
|
||||
await Promise.all(_temporaryFiles.map(name => fs.remove(name)));
|
||||
});
|
||||
|
||||
describe('by default', () => {
|
||||
@@ -82,445 +94,339 @@ describe('CLI', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('(jest)', () => {
|
||||
beforeEach(() => {
|
||||
detoxConfig.testRunner = 'jest';
|
||||
});
|
||||
|
||||
describe('given no extra args (iOS)', () => {
|
||||
beforeEach(async () => {
|
||||
singleConfig().device.type = 'ios.simulator';
|
||||
await run();
|
||||
});
|
||||
|
||||
test('should produce a default command (integration test, ios)', () => {
|
||||
const args = `--config e2e/config.json --testNamePattern ${quote('^((?!:android:).)*$')}`;
|
||||
expect(cliCall().command).toBe(`jest ${args}`);
|
||||
});
|
||||
|
||||
test('should put default environment variables (integration test, ios)', () => {
|
||||
expect(cliCall().env).toEqual({
|
||||
DETOX_START_TIMESTAMP: expect.any(Number),
|
||||
DETOX_CONFIG_PATH: expect.any(String),
|
||||
DETOX_REPORT_SPECS: true,
|
||||
DETOX_USE_CUSTOM_LOGGER: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given no extra args (Android)', () => {
|
||||
beforeEach(async () => {
|
||||
singleConfig().device.type = 'android.emulator';
|
||||
await run();
|
||||
});
|
||||
|
||||
test('should produce a default command (integration test)', () => {
|
||||
const args = `--config e2e/config.json --testNamePattern ${quote('^((?!:ios:).)*$')}`;
|
||||
expect(cliCall().command).toBe(`jest ${args}`);
|
||||
});
|
||||
|
||||
test('should put default environment variables (integration test)', () => {
|
||||
expect(cliCall().env).toEqual({
|
||||
DETOX_START_TIMESTAMP: expect.any(Number),
|
||||
DETOX_CONFIG_PATH: expect.any(String),
|
||||
DETOX_REPORT_SPECS: true,
|
||||
DETOX_USE_CUSTOM_LOGGER: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.each([['-c'], ['--configuration']])(
|
||||
'%s <configuration> should provide inverted --testNamePattern that configuration (jest)',
|
||||
async (__configuration) => {
|
||||
detoxConfig.configurations.iosTest = _.cloneDeep(detoxConfig.configurations.single);
|
||||
detoxConfig.configurations.iosTest.device.type = 'ios.simulator';
|
||||
detoxConfig.configurations.androidTest = _.cloneDeep(detoxConfig.configurations.single);
|
||||
detoxConfig.configurations.androidTest.device.type = 'android.emulator';
|
||||
|
||||
await run(`${__configuration} androidTest`);
|
||||
expect(cliCall(0).command).toContain(`--testNamePattern ${quote('^((?!:ios:).)*$')}`);
|
||||
expect(cliCall(0).env.DETOX_CONFIGURATION).toBe('androidTest');
|
||||
|
||||
await run(`${__configuration} iosTest`);
|
||||
expect(cliCall(1).command).toContain(`--testNamePattern ${quote('^((?!:android:).)*$')}`);
|
||||
expect(cliCall(1).env.DETOX_CONFIGURATION).toBe('iosTest');
|
||||
}
|
||||
);
|
||||
|
||||
test.each([['-o'], ['--runner-config']])('%s <path> should point to the specified Jest config', async (__runnerConfig) => {
|
||||
await run(`${__runnerConfig} e2e/custom.config.js`);
|
||||
expect(cliCall().command).toContain(`--config e2e/custom.config.js`);
|
||||
});
|
||||
|
||||
test.each([['-l'], ['--loglevel']])('%s <value> should be passed as environment variable', async (__loglevel) => {
|
||||
await run(`${__loglevel} trace`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_LOGLEVEL: 'trace' }));
|
||||
});
|
||||
|
||||
test('--no-color should be passed as CLI argument', async () => {
|
||||
await run(`--no-color`);
|
||||
expect(cliCall().command).toContain(' --no-color ');
|
||||
});
|
||||
|
||||
test.each([['-R'], ['--retries']])('%s <value> should execute successful run once', async (__retries) => {
|
||||
await run(`-R 1`);
|
||||
expect(cliCall(1)).toBe(null);
|
||||
});
|
||||
|
||||
test.each([['-R'], ['--retries']])('%s <value> should clear failed tests file', async (__retries) => {
|
||||
await run(`-R 1`);
|
||||
const { resetLastFailedTests } = require('../src/utils/lastFailedTests');
|
||||
expect(resetLastFailedTests).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each([['-R'], ['--retries']])('%s <value> should execute unsuccessful run N extra times', async (__retries) => {
|
||||
const { loadLastFailedTests } = require('../src/utils/lastFailedTests');
|
||||
loadLastFailedTests.mockReturnValueOnce(['e2e/failing1.test.js', 'e2e/failing2.test.js']);
|
||||
loadLastFailedTests.mockReturnValueOnce(['e2e/failing2.test.js']);
|
||||
cp.execSync.mockImplementation(() => { throw new Error; });
|
||||
|
||||
await run(`-R 2`).catch(_.noop);
|
||||
expect(cliCall(0).env).not.toHaveProperty('DETOX_RERUN_INDEX');
|
||||
|
||||
expect(cliCall(1).command).toMatch(/ e2e\/failing1.test.js e2e\/failing2.test.js$/);
|
||||
expect(cliCall(1).env.DETOX_RERUN_INDEX).toBe(1);
|
||||
|
||||
expect(cliCall(2).command).toMatch(/ e2e\/failing2.test.js$/);
|
||||
expect(cliCall(2).env.DETOX_RERUN_INDEX).toBe(2);
|
||||
});
|
||||
|
||||
test.each([['-R'], ['--retries']])('%s <value> should not restart test runner if there are no failing tests paths', async (__retries) => {
|
||||
const { loadLastFailedTests } = require('../src/utils/lastFailedTests');
|
||||
loadLastFailedTests.mockReturnValueOnce([]);
|
||||
cp.execSync.mockImplementation(() => { throw new Error; });
|
||||
|
||||
await run(`-R 1`).catch(_.noop);
|
||||
expect(cliCall(0)).not.toBe(null);
|
||||
expect(cliCall(1)).toBe(null);
|
||||
});
|
||||
|
||||
test.each([['-R'], ['--retries']])('%s <value> should retain -- <...explicitPassthroughArgs>', async (__retries) => {
|
||||
const { loadLastFailedTests } = require('../src/utils/lastFailedTests');
|
||||
loadLastFailedTests.mockReturnValue(['tests/failing.test.js']);
|
||||
cp.execSync.mockImplementation(() => { throw new Error; });
|
||||
|
||||
await run(`-R 1 tests -- --debug`).catch(_.noop);
|
||||
expect(cliCall(0).command).toMatch(/ --debug .* tests$/);
|
||||
expect(cliCall(1).command).toMatch(/ --debug .* tests\/failing.test.js$/);
|
||||
});
|
||||
|
||||
test.each([['-r'], ['--reuse']])('%s <value> should be passed as environment variable', async (__reuse) => {
|
||||
await run(`${__reuse}`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_REUSE: true }));
|
||||
});
|
||||
|
||||
test.each([['-u'], ['--cleanup']])('%s <value> should be passed as environment variable', async (__cleanup) => {
|
||||
await run(`${__cleanup}`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_CLEANUP: true }));
|
||||
});
|
||||
|
||||
test.each([['-d'], ['--debug-synchronization']])('%s <value> should be passed as environment variable', async (__debug_synchronization) => {
|
||||
await run(`${__debug_synchronization} 5000`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_DEBUG_SYNCHRONIZATION: 5000 }));
|
||||
});
|
||||
|
||||
test.each([['-d'], ['--debug-synchronization']])('%s <value> should be passed as 0 when given false', async (__debug_synchronization) => {
|
||||
await run(`${__debug_synchronization} false`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_DEBUG_SYNCHRONIZATION: 0 }));
|
||||
});
|
||||
|
||||
test.each([['-d'], ['--debug-synchronization']])('%s <value> should have default value = 3000', async (__debug_synchronization) => {
|
||||
await run(`${__debug_synchronization}`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_DEBUG_SYNCHRONIZATION: 3000 }));
|
||||
});
|
||||
|
||||
test.each([['-a'], ['--artifacts-location']])('%s <value> should be passed as environment variable', async (__artifacts_location) => {
|
||||
await run(`${__artifacts_location} /tmp`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_ARTIFACTS_LOCATION: '/tmp' }));
|
||||
});
|
||||
|
||||
test('--record-logs <value> should be passed as environment variable', async () => {
|
||||
await run(`--record-logs all`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_RECORD_LOGS: 'all' }));
|
||||
});
|
||||
|
||||
test('--take-screenshots <value> should be passed as environment variable', async () => {
|
||||
await run(`--take-screenshots failing`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_TAKE_SCREENSHOTS: 'failing' }));
|
||||
});
|
||||
|
||||
test('--record-videos <value> should be passed as environment variable', async () => {
|
||||
await run(`--record-videos failing`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_RECORD_VIDEOS: 'failing' }));
|
||||
});
|
||||
|
||||
test('--record-performance <value> should be passed as environment variable', async () => {
|
||||
await run(`--record-performance all`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_RECORD_PERFORMANCE: 'all' }));
|
||||
});
|
||||
|
||||
test('--record-timeline <value> should be passed as environment variable', async () => {
|
||||
await run(`--record-timeline all`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_RECORD_TIMELINE: 'all' }));
|
||||
});
|
||||
|
||||
test('--capture-view-hierarchy <value> should be passed as environment variable', async () => {
|
||||
await run(`--capture-view-hierarchy enabled`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_CAPTURE_VIEW_HIERARCHY: 'enabled' }));
|
||||
});
|
||||
|
||||
test.each([['-w'], ['--workers']])('%s <value> should be passed as CLI argument', async (__workers) => {
|
||||
await run(`${__workers} 2`);
|
||||
expect(cliCall().command).toContain('--maxWorkers 2');
|
||||
});
|
||||
|
||||
test.each([['-w'], ['--workers']])('%s <value> should be replaced with --maxWorkers <value>', async (__workers) => {
|
||||
await run(`${__workers} 2 --maxWorkers 3`);
|
||||
|
||||
const { command } = cliCall();
|
||||
expect(command).toContain('--maxWorkers 3');
|
||||
expect(command).not.toContain('--maxWorkers 2');
|
||||
});
|
||||
|
||||
test.each([['-w'], ['--workers']])('%s <value> can be overriden by a later value', async (__workers) => {
|
||||
await run(`${__workers} 2 ${__workers} 3`);
|
||||
|
||||
const { command } = cliCall();
|
||||
expect(command).toContain('--maxWorkers 3');
|
||||
expect(command).not.toContain('--maxWorkers 2');
|
||||
});
|
||||
|
||||
test.each([['-w'], ['--workers']])('%s <value> should not warn anything for iOS', async (__workers) => {
|
||||
singleConfig().device.type = 'ios.simulator';
|
||||
await run(`${__workers} 2`);
|
||||
expect(logger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each([['-w'], ['--workers']])('%s <value> should not put readOnlyEmu environment variable for iOS', async (__workers) => {
|
||||
singleConfig().device.type = 'ios.simulator';
|
||||
await run(`${__workers} 2`);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_READ_ONLY_EMU');
|
||||
});
|
||||
|
||||
test.each([['-w'], ['--workers']])('%s <value> should not put readOnlyEmu environment variable for android.attached', async (__workers) => {
|
||||
singleConfig().device.type = 'android.attached';
|
||||
await run(`${__workers} 2`);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_READ_ONLY_EMU');
|
||||
});
|
||||
|
||||
test.each([['-w'], ['--workers']])('%s <value> should not put readOnlyEmu environment variable for android.emulator if there is a single worker', async (__workers) => {
|
||||
singleConfig().device.type = 'android.emulator';
|
||||
await run(`${__workers} 1`);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_READ_ONLY_EMU');
|
||||
});
|
||||
|
||||
test.each([['-w'], ['--workers']])('%s <value> should put readOnlyEmu environment variable for Android if there are multiple workers', async (__workers) => {
|
||||
singleConfig().device.type = 'android.emulator';
|
||||
await run(`${__workers} 2`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_READ_ONLY_EMU: true }));
|
||||
});
|
||||
|
||||
test('should omit --testNamePattern for custom platforms', async () => {
|
||||
singleConfig().device.type = tempfile('.js', aCustomDriverModule());
|
||||
|
||||
describe.each([
|
||||
['iOS', 'ios.simulator'],
|
||||
['Android', 'android.emulator'],
|
||||
])('given no extra args (%s)', (_platform, deviceType) => {
|
||||
beforeEach(async () => {
|
||||
singleConfig().device.type = deviceType;
|
||||
await run();
|
||||
expect(cliCall().command).not.toContain('--testNamePattern');
|
||||
});
|
||||
|
||||
test.each([['-t'], ['--testNamePattern']])('should override --testNamePattern if a custom %s value is passed', async (__testNamePattern) => {
|
||||
await run(`${__testNamePattern} customPattern`);
|
||||
const { command } = cliCall();
|
||||
|
||||
expect(command).not.toMatch(/--testNamePattern .*(ios|android)/);
|
||||
expect(command).toMatch(/--testNamePattern customPattern($| )/);
|
||||
test('should produce a default command', () => {
|
||||
expect(cliCall().argv).toEqual([expect.stringContaining('executable'), '--config', 'e2e/config.json']);
|
||||
});
|
||||
|
||||
test('--jest-report-specs, by default, should be true, as environment variable', async () => {
|
||||
await run();
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_REPORT_SPECS: true }));
|
||||
test('should not override environment variables', () => {
|
||||
expect(cliCall().env).toEqual({ DETOX_CONFIG_SNAPSHOT_PATH: expect.any(String) });
|
||||
});
|
||||
|
||||
test('--jest-report-specs, by default, should be false, if multiple workers are enabled', async () => {
|
||||
await run('--workers 2');
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_REPORT_SPECS: false }));
|
||||
test('should hint essential environment variables', () => {
|
||||
expect(cliCall().fullCommand).toMatch(/^DETOX_CONFIG_PATH=.*\bexecutable/);
|
||||
});
|
||||
});
|
||||
|
||||
test('--jest-report-specs, set explicitly, should override single worker defaults', async () => {
|
||||
await run('--jest-report-specs false');
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_REPORT_SPECS: false }));
|
||||
});
|
||||
test('should use testRunner.args._ as default specs', async () => {
|
||||
detoxConfig.testRunner.args._ = ['e2e/sanity'];
|
||||
await run();
|
||||
expect(_.last(cliCall().argv)).toEqual('e2e/sanity');
|
||||
});
|
||||
|
||||
test('--jest-report-specs, set explicitly, should override multiple workers defaults', async () => {
|
||||
await run('--workers 2 --jest-report-specs');
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_REPORT_SPECS: true }));
|
||||
});
|
||||
test.each([['--config']])('%s <path> should point to the specified Jest config', async (__runnerConfig) => {
|
||||
await run(__runnerConfig, 'e2e/custom.config.js');
|
||||
expect(cliCall().argv).toEqual([expect.stringContaining('executable'), '--config', 'e2e/custom.config.js']);
|
||||
});
|
||||
|
||||
test.each([['-H'], ['--headless']])('%s <value> should be passed as environment variable', async (__headless) => {
|
||||
await run(`${__headless}`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_HEADLESS: true }));
|
||||
});
|
||||
test.each([['-l'], ['--loglevel']])('%s <value> should be passed as environment variable', async (__loglevel) => {
|
||||
await run(__loglevel, 'trace');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_LOGLEVEL');
|
||||
expect(cliCall().fullCommand).toMatch(/ DETOX_LOGLEVEL="trace" /);
|
||||
});
|
||||
|
||||
test('--gpu <value> should be passed as environment variable', async () => {
|
||||
await run(`--gpu angle_indirect`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_GPU: 'angle_indirect' }));
|
||||
});
|
||||
test.each([['-R'], ['--retries']])('%s <value> should execute successful run once', async (__retries) => {
|
||||
await run(__retries, 1);
|
||||
expect(cliCall(1)).toBe(null);
|
||||
});
|
||||
|
||||
test('--device-boot-args should be passed as an environment variable (without deprecation warnings)', async () => {
|
||||
await run(`--device-boot-args "--verbose"`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({
|
||||
DETOX_DEVICE_BOOT_ARGS: '--verbose'
|
||||
}));
|
||||
expect(logger.warn).not.toHaveBeenCalledWith(DEVICE_LAUNCH_ARGS_DEPRECATION);
|
||||
});
|
||||
test.each([['-R'], ['--retries']])('%s <value> should execute unsuccessful run N extra times', async (__retries) => {
|
||||
const context = require('../internals');
|
||||
context.session.testFilesToRetry = ['e2e/failing1.test.js', 'e2e/failing2.test.js'];
|
||||
context.session.testFilesToRetry.splice = jest.fn()
|
||||
.mockImplementationOnce(() => ['e2e/failing1.test.js', 'e2e/failing2.test.js'])
|
||||
.mockImplementationOnce(() => ['e2e/failing2.test.js']);
|
||||
|
||||
test('--device-launch-args should serve as a deprecated alias to --device-boot-args', async () => {
|
||||
await run(`--device-launch-args "--verbose"`);
|
||||
expect(cliCall().env.DETOX_DEVICE_BOOT_ARGS).toBe('--verbose');
|
||||
expect(logger.warn).toHaveBeenCalledWith(DEVICE_LAUNCH_ARGS_DEPRECATION);
|
||||
});
|
||||
mockExitCode(1);
|
||||
|
||||
test('--app-launch-args should be passed as an environment variable', async () => {
|
||||
await run(`--app-launch-args "--debug yes"`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({
|
||||
DETOX_APP_LAUNCH_ARGS: '--debug yes',
|
||||
}));
|
||||
});
|
||||
await run(__retries, 2).catch(_.noop);
|
||||
|
||||
test('--use-custom-logger false should be prevent passing environment variable', async () => {
|
||||
await run(`--use-custom-logger false`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({
|
||||
DETOX_USE_CUSTOM_LOGGER: false
|
||||
}));
|
||||
});
|
||||
expect(cliCall(0).argv).toEqual([expect.stringMatching(/executable$/), '--config', 'e2e/config.json']);
|
||||
expect(cliCall(1).argv).toEqual([expect.stringMatching(/executable$/), '--config', 'e2e/config.json', 'e2e/failing1.test.js', 'e2e/failing2.test.js']);
|
||||
expect(cliCall(2).argv).toEqual([expect.stringMatching(/executable$/), '--config', 'e2e/config.json', 'e2e/failing2.test.js']);
|
||||
});
|
||||
|
||||
test('--force-adb-install should be ignored for iOS', async () => {
|
||||
singleConfig().device.type = 'ios.simulator';
|
||||
await run(`--force-adb-install`);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_FORCE_ADB_INSTALL');
|
||||
});
|
||||
test.each([['-R'], ['--retries']])('%s <value> should bail if has permanently failed tests', async (__retries) => {
|
||||
const context = require('../internals');
|
||||
context.session.failedTestFiles = ['e2e/failing1.test.js'];
|
||||
context.session.testFilesToRetry = ['e2e/failing2.test.js'];
|
||||
|
||||
test('--force-adb-install should be passed as environment variable', async () => {
|
||||
singleConfig().device.type = 'android.emulator';
|
||||
await run(`--force-adb-install`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({
|
||||
DETOX_FORCE_ADB_INSTALL: true,
|
||||
}));
|
||||
});
|
||||
mockExitCode(1);
|
||||
|
||||
test.each([['-n'], ['--device-name']])('%s <value> should be passed as environment variable', async (__device_name) => {
|
||||
await run(`${__device_name} TheDevice`);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({
|
||||
DETOX_DEVICE_NAME: 'TheDevice',
|
||||
}));
|
||||
});
|
||||
await run(__retries, 2).catch(_.noop);
|
||||
|
||||
test('specifying direct test paths', async () => {
|
||||
await run(`e2e/01.sanity.test.js e2e/02.sanity.test.js`);
|
||||
expect(cliCall().command).not.toMatch(/ e2e /);
|
||||
expect(cliCall().command).not.toMatch(/ e2e$/);
|
||||
expect(cliCall().command).toMatch(/ e2e\/01.sanity.test.js e2e\/02.sanity.test.js$/);
|
||||
});
|
||||
expect(cliCall(0).env).not.toHaveProperty('DETOX_RERUN_INDEX');
|
||||
expect(cliCall(0).argv).toEqual([expect.stringMatching(/executable$/), '--config', 'e2e/config.json']);
|
||||
expect(cliCall(1)).toBe(null);
|
||||
});
|
||||
|
||||
// TODO: fix --inspect-brk behavior on Windows, and replace (cmd|js) with js here
|
||||
test.each([
|
||||
['--inspect-brk e2eFolder', /^node --inspect-brk .*jest\.(?:cmd|js) .* e2eFolder$/, {}],
|
||||
['-d e2eFolder', / e2eFolder$/, { DETOX_DEBUG_SYNCHRONIZATION: 3000 }],
|
||||
['--debug-synchronization e2eFolder', / e2eFolder$/, { DETOX_DEBUG_SYNCHRONIZATION: 3000 }],
|
||||
['-r e2eFolder', / e2eFolder$/, { DETOX_REUSE: true }],
|
||||
['--reuse e2eFolder', / e2eFolder$/, { DETOX_REUSE: true }],
|
||||
['-u e2eFolder', / e2eFolder$/, { DETOX_CLEANUP: true }],
|
||||
['--cleanup e2eFolder', / e2eFolder$/, { DETOX_CLEANUP: true }],
|
||||
['--jest-report-specs e2eFolder', / e2eFolder$/, { DETOX_REPORT_SPECS: true }],
|
||||
['-H e2eFolder', / e2eFolder$/, { DETOX_HEADLESS: true }],
|
||||
['--headless e2eFolder', / e2eFolder$/, { DETOX_HEADLESS: true }],
|
||||
['--keepLockFile e2eFolder', / e2eFolder$/, {}],
|
||||
['--use-custom-logger e2eFolder', / e2eFolder$/, { DETOX_USE_CUSTOM_LOGGER: true }],
|
||||
['--force-adb-install e2eFolder', / e2eFolder$/, { DETOX_FORCE_ADB_INSTALL: true }],
|
||||
])('"%s" should be disambigued correctly', async (command, commandMatcher, envMatcher) => {
|
||||
singleConfig().device.type = 'android.emulator';
|
||||
await run(command);
|
||||
test.each([['-R'], ['--retries']])('%s <value> should not restart test runner if there are no failing tests paths', async (__retries) => {
|
||||
mockExitCode(1);
|
||||
|
||||
expect(cliCall().command).toMatch(commandMatcher);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining(envMatcher));
|
||||
});
|
||||
await run(__retries, 1).catch(_.noop);
|
||||
expect(cliCall(0)).not.toBe(null);
|
||||
expect(cliCall(1)).toBe(null);
|
||||
});
|
||||
|
||||
test('e.g., --debug should be passed through', async () => {
|
||||
await run(`--debug`);
|
||||
expect(cliCall().command).toContain('--debug');
|
||||
});
|
||||
test.each([['-R'], ['--retries']])('%s <value> should retain -- <...explicitPassthroughArgs>', async (__retries) => {
|
||||
const context = require('../internals');
|
||||
context.session.testFilesToRetry = ['tests/failing.test.js'];
|
||||
|
||||
test('e.g., --coverageProvider v8 should be passed through', async () => {
|
||||
await run(`--coverageProvider v8`);
|
||||
expect(cliCall().command).toContain('--coverageProvider v8');
|
||||
});
|
||||
mockExitCode(1);
|
||||
|
||||
test('e.g., --debug e2e/Login.test.js should be split to --debug and e2e/Login.test.js', async () => {
|
||||
await run(`--debug e2e/Login.test.js --coverageProvider v8`);
|
||||
expect(cliCall().command).toMatch(/--debug --coverageProvider v8 e2e\/Login.test.js$/);
|
||||
});
|
||||
await run(__retries, 1, 'tests', '--', '--debug').catch(_.noop);
|
||||
|
||||
test.each([
|
||||
[`--testNamePattern "should tap"`, `--testNamePattern ${quote('should tap')}`],
|
||||
[`"e2e tests/first test.spec.js"`, `"e2e tests/first test.spec.js"`],
|
||||
])('should escape %s when forwarding it as a CLI argument', async (cmd, expected) => {
|
||||
await run(cmd);
|
||||
expect(cliCall().command).toContain(` ${expected}`);
|
||||
});
|
||||
expect(cliCall(0).argv).toEqual([expect.stringMatching(/executable$/), '--config', 'e2e/config.json', '--debug', 'tests']);
|
||||
expect(cliCall(1).argv).toEqual([expect.stringMatching(/executable$/), '--config', 'e2e/config.json', '--debug', 'tests/failing.test.js']);
|
||||
});
|
||||
|
||||
test(`should deduce wrapped jest CLI`, async () => {
|
||||
detoxConfig.testRunner = `nyc jest`;
|
||||
await run();
|
||||
expect(cliCall().command).toMatch(RegExp(`nyc jest `));
|
||||
});
|
||||
test.each([['-r'], ['--reuse']])('%s <value> should be passed as environment variable', async (__reuse) => {
|
||||
await run(__reuse);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_REUSE');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_REUSE=true /);
|
||||
});
|
||||
|
||||
describe.each([['ios.simulator'], ['android.emulator']])('for %s', (deviceType) => {
|
||||
beforeEach(() => {
|
||||
Object.values(detoxConfig.configurations)[0].device.type = deviceType;
|
||||
});
|
||||
test.each([['-u'], ['--cleanup']])('%s <value> should be passed as environment variable', async (__cleanup) => {
|
||||
await run(__cleanup);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_CLEANUP');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_CLEANUP=true /);
|
||||
});
|
||||
|
||||
test('--keepLockFile should be suppress clearing the device lock file', async () => {
|
||||
await run('--keepLockFile');
|
||||
expect(DeviceRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
test.each([['-d'], ['--debug-synchronization']])('%s <value> should be passed as environment variable', async (__debug_synchronization) => {
|
||||
await run(__debug_synchronization, 5000);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_DEBUG_SYNCHRONIZATION');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_DEBUG_SYNCHRONIZATION=5000 /);
|
||||
});
|
||||
|
||||
test('--keepLockFile omission means clearing the device lock file', async () => {
|
||||
await run();
|
||||
expect(DeviceRegistry.mock.instances[0].reset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
test.each([['-d'], ['--debug-synchronization']])('%s <value> should be passed as 0 when given false', async (__debug_synchronization) => {
|
||||
await run(__debug_synchronization, false);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_DEBUG_SYNCHRONIZATION');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_DEBUG_SYNCHRONIZATION=0 /);
|
||||
});
|
||||
|
||||
test('-- <...explicitPassthroughArgs> should be forwarded to the test runner CLI as-is', async () => {
|
||||
await run('--device-boot-args detoxArgs e2eFolder -- a -a --a --device-boot-args runnerArgs');
|
||||
expect(cliCall().command).toMatch(/a -a --a --device-boot-args runnerArgs .* e2eFolder$/);
|
||||
expect(cliCall().env).toEqual(expect.objectContaining({ DETOX_DEVICE_BOOT_ARGS: 'detoxArgs' }));
|
||||
});
|
||||
test.each([['-d'], ['--debug-synchronization']])('%s <value> should have default value = 3000', async (__debug_synchronization) => {
|
||||
await run(`${__debug_synchronization}`);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_DEBUG_SYNCHRONIZATION');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_DEBUG_SYNCHRONIZATION=3000 /);
|
||||
});
|
||||
|
||||
test('-- <...explicitPassthroughArgs> should omit double-dash "--" itself, when forwarding args', async () => {
|
||||
await run('./detox -- --forceExit');
|
||||
test.each([['-a'], ['--artifacts-location']])('%s <value> should be passed as environment variable', async (__artifacts_location) => {
|
||||
await run(__artifacts_location, '/tmp');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_ARTIFACTS_LOCATION');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_ARTIFACTS_LOCATION="\/tmp" /);
|
||||
});
|
||||
|
||||
expect(cliCall().command).toMatch(/ --forceExit .* \.\/detox$/);
|
||||
expect(cliCall().command).not.toMatch(/ -- --forceExit .* \.\/detox$/);
|
||||
});
|
||||
test('--record-logs <value> should be passed as environment variable', async () => {
|
||||
await run('--record-logs', 'all');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_RECORD_LOGS');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_RECORD_LOGS="all" /);
|
||||
});
|
||||
|
||||
test('--inspect-brk should prepend "node --inspect-brk" to the command', async () => {
|
||||
// TODO: fix --inspect-brk behavior on Windows
|
||||
if (process.platform === 'win32') return;
|
||||
test('--take-screenshots <value> should be passed as environment variable', async () => {
|
||||
await run('--take-screenshots', 'failing');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_TAKE_SCREENSHOTS');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_TAKE_SCREENSHOTS="failing" /);
|
||||
});
|
||||
|
||||
await run('--inspect-brk');
|
||||
const absolutePathToTestRunnerJs = require.resolve(`.bin/jest`);
|
||||
expect(cliCall().command).toMatch(RegExp(`^node --inspect-brk ${absolutePathToTestRunnerJs}`));
|
||||
});
|
||||
test('--record-videos <value> should be passed as environment variable', async () => {
|
||||
await run('--record-videos', 'failing');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_RECORD_VIDEOS');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_RECORD_VIDEOS="failing" /);
|
||||
});
|
||||
|
||||
test('should append $DETOX_ARGV_OVERRIDE to detox test ... command and print a warning', async () => {
|
||||
process.env.PLATFORM = 'ios';
|
||||
process.env.DETOX_ARGV_OVERRIDE = '--inspect-brk --testNamePattern "[$PLATFORM] tap" e2e/sanity/*.test.js';
|
||||
await run();
|
||||
test('--record-performance <value> should be passed as environment variable', async () => {
|
||||
await run('--record-performance', 'all');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_RECORD_PERFORMANCE');
|
||||
expect(cliCall().fullCommand).toMatch(/\DETOX_RECORD_PERFORMANCE="all" /);
|
||||
});
|
||||
|
||||
const pattern = new RegExp(`^node --inspect-brk.* --testNamePattern ${quote('\\[ios\\] tap')}.* e2e/sanity/\\*\\.test.js$`);
|
||||
test('--capture-view-hierarchy <value> should be passed as environment variable', async () => {
|
||||
await run('--capture-view-hierarchy', 'enabled');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_CAPTURE_VIEW_HIERARCHY');
|
||||
expect(cliCall().fullCommand).toMatch(/\DETOX_CAPTURE_VIEW_HIERARCHY="enabled" /);
|
||||
});
|
||||
|
||||
expect(cliCall().command).toMatch(pattern);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('$DETOX_ARGV_OVERRIDE is detected'));
|
||||
});
|
||||
test('--jest-report-specs, set explicitly, should be passed as an environment variable', async () => {
|
||||
await run('--jest-report-specs');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_REPORT_SPECS');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_REPORT_SPECS=true /);
|
||||
});
|
||||
|
||||
test.each([['-H'], ['--headless']])('%s <value> should be passed as environment variable', async (__headless) => {
|
||||
await run(__headless);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_HEADLESS');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_HEADLESS=true /);
|
||||
});
|
||||
|
||||
test('--gpu <value> should be passed as environment variable', async () => {
|
||||
await run('--gpu', 'angle_indirect');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_GPU');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_GPU="angle_indirect" /);
|
||||
});
|
||||
|
||||
test('--device-boot-args should be passed as an environment variable (without deprecation warnings)', async () => {
|
||||
await run('--device-boot-args="--verbose"');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_DEVICE_BOOT_ARGS');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_DEVICE_BOOT_ARGS="--verbose" /);
|
||||
expect(logger().warn).not.toHaveBeenCalledWith(DEVICE_LAUNCH_ARGS_DEPRECATION);
|
||||
});
|
||||
|
||||
test('--device-launch-args should serve as a deprecated alias to --device-boot-args', async () => {
|
||||
await run(`--device-launch-args="--verbose"`);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_DEVICE_BOOT_ARGS');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_DEVICE_BOOT_ARGS="--verbose" /);
|
||||
expect(logger().warn).toHaveBeenCalledWith(DEVICE_LAUNCH_ARGS_DEPRECATION);
|
||||
});
|
||||
|
||||
test('--app-launch-args should be passed as an environment variable', async () => {
|
||||
await run(`--app-launch-args="--debug yes"`);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_APP_LAUNCH_ARGS');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_APP_LAUNCH_ARGS="--debug yes" /);
|
||||
});
|
||||
|
||||
test('--use-custom-logger false should be prevent passing environment variable', async () => {
|
||||
await run(`--use-custom-logger=false`);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_USE_CUSTOM_LOGGER');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_USE_CUSTOM_LOGGER=false /);
|
||||
});
|
||||
|
||||
test('--force-adb-install should be ignored for iOS', async () => {
|
||||
singleConfig().device.type = 'ios.simulator';
|
||||
await run(`--force-adb-install`);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_FORCE_ADB_INSTALL');
|
||||
expect(cliCall().fullCommand).not.toMatch(/DETOX_FORCE_ADB_INSTALL/);
|
||||
});
|
||||
|
||||
test('--force-adb-install should be passed as environment variable', async () => {
|
||||
singleConfig().device.type = 'android.emulator';
|
||||
await run(`--force-adb-install`);
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_FORCE_ADB_INSTALL');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_FORCE_ADB_INSTALL=true /);
|
||||
});
|
||||
|
||||
test.each([['-n'], ['--device-name']])('%s <value> should be passed as environment variable', async (__device_name) => {
|
||||
await run(__device_name, 'TheDevice');
|
||||
expect(cliCall().env).not.toHaveProperty('DETOX_DEVICE_NAME');
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_DEVICE_NAME="TheDevice" /);
|
||||
});
|
||||
|
||||
test('specifying direct test paths instead of default args._', async () => {
|
||||
detoxConfig.testRunner.args._ = ['e2e/'];
|
||||
await run('e2e/01.sanity.test.js', 'e2e/02.sanity.test.js');
|
||||
|
||||
expect(cliCall().argv).not.toContain('e2e');
|
||||
expect(cliCall().argv.slice(-2)).toEqual(['e2e/01.sanity.test.js', 'e2e/02.sanity.test.js']);
|
||||
});
|
||||
|
||||
test.todo('--inspect-brk should work');
|
||||
|
||||
test.each([
|
||||
['-d e2eFolder', / e2eFolder$/, /\bDETOX_DEBUG_SYNCHRONIZATION=3000/],
|
||||
['--debug-synchronization e2eFolder', / e2eFolder$/, /\bDETOX_DEBUG_SYNCHRONIZATION=3000/],
|
||||
['-r e2eFolder', / e2eFolder$/, /\bDETOX_REUSE=true/],
|
||||
['--reuse e2eFolder', / e2eFolder$/, /\bDETOX_REUSE=true/],
|
||||
['-u e2eFolder', / e2eFolder$/, /\bDETOX_CLEANUP=true/],
|
||||
['--cleanup e2eFolder', / e2eFolder$/, /\bDETOX_CLEANUP=true/],
|
||||
['--jest-report-specs e2eFolder', / e2eFolder$/, /\bDETOX_REPORT_SPECS=true/],
|
||||
['-H e2eFolder', / e2eFolder$/, /\bDETOX_HEADLESS=true/],
|
||||
['--headless e2eFolder', / e2eFolder$/, /\bDETOX_HEADLESS=true/],
|
||||
['--keepLockFile e2eFolder', / e2eFolder$/, /\bDETOX_KEEP_LOCKFILE=true/],
|
||||
['--use-custom-logger e2eFolder', / e2eFolder$/, /\bDETOX_USE_CUSTOM_LOGGER=true/],
|
||||
['--force-adb-install e2eFolder', / e2eFolder$/, /\bDETOX_FORCE_ADB_INSTALL=true/],
|
||||
])('"%s" should be disambigued correctly', async (command, commandMatcher, envMatcher) => {
|
||||
singleConfig().device.type = 'android.emulator';
|
||||
await run(...command.split(' '));
|
||||
|
||||
expect(cliCall().argv.join(' ')).toMatch(commandMatcher);
|
||||
expect(cliCall().fullCommand).toEqual(expect.objectContaining(envMatcher));
|
||||
});
|
||||
|
||||
test('e.g., --debug should be passed through', async () => {
|
||||
await run(`--debug`);
|
||||
expect(cliCall().argv).toContain('--debug');
|
||||
});
|
||||
|
||||
test('e.g., --coverageProvider v8 should be passed through', async () => {
|
||||
await run('--coverageProvider', 'v8');
|
||||
expect(cliCall().argv.slice(-2)).toEqual(['--coverageProvider', 'v8']);
|
||||
});
|
||||
|
||||
test('e.g., --debug e2e/Login.test.js should be split to --debug and e2e/Login.test.js', async () => {
|
||||
await run('--debug', 'e2e/Login.test.js', '--coverageProvider', 'v8');
|
||||
|
||||
expect(cliCall().argv).toEqual([
|
||||
expect.stringMatching(/executable$/),
|
||||
'--config', 'e2e/config.json',
|
||||
'--debug',
|
||||
'--coverageProvider', 'v8',
|
||||
'e2e/Login.test.js'
|
||||
]);
|
||||
});
|
||||
|
||||
test('should escape whitespaces when forwarding a CLI argument', async () => {
|
||||
await run(`e2e tests/first test.spec.js`);
|
||||
expect(_.last(cliCall().argv)).toEqual(`e2e tests/first test.spec.js`);
|
||||
});
|
||||
|
||||
test(`should be able to use custom test runner commands`, async () => {
|
||||
detoxConfig.testRunner.args.$0 += ' --hello';
|
||||
await run();
|
||||
expect(cliCall().argv).toContain('--hello');
|
||||
});
|
||||
|
||||
test('-- <...explicitPassthroughArgs> should be forwarded to the test runner CLI as-is', async () => {
|
||||
await run('--device-boot-args', 'detoxArgs', 'e2eFolder', '--', 'a', '-a', '--a', '--device-boot-args', 'runnerArgs');
|
||||
expect(cliCall().argv).toEqual([
|
||||
expect.stringMatching(/executable$/),
|
||||
'--config', 'e2e/config.json',
|
||||
'a',
|
||||
'-a',
|
||||
'--a',
|
||||
'--device-boot-args',
|
||||
'runnerArgs',
|
||||
'e2eFolder',
|
||||
]);
|
||||
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_DEVICE_BOOT_ARGS="detoxArgs" /);
|
||||
});
|
||||
|
||||
test('-- <...explicitPassthroughArgs> should omit double-dash "--" only once when forwarding args', async () => {
|
||||
await run('--', '--', '--deepParameter');
|
||||
|
||||
expect(cliCall().argv).toContain('--');
|
||||
expect(cliCall().argv).toContain('--deepParameter');
|
||||
});
|
||||
|
||||
// TODO: revive this test
|
||||
test.skip('--inspect-brk should prepend "node --inspect-brk" to the command', async () => {
|
||||
await run('--inspect-brk');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
expect(cliCall().argv).toMatch(/^node --inspect-brk \.\/node_modules\/jest\/bin\/jest\.js/);
|
||||
} else {
|
||||
expect(cliCall().argv).toMatch(/^node --inspect-brk \.\/node_modules\/\.bin\/jest/);
|
||||
}
|
||||
});
|
||||
|
||||
test('should append $DETOX_ARGV_OVERRIDE to detox test ... command and print a warning', async () => {
|
||||
process.env.PLATFORM = 'ios';
|
||||
process.env.DETOX_ARGV_OVERRIDE = os.platform() === 'win32'
|
||||
? '--testNamePattern="[%PLATFORM%] tap" -l trace e2e/sanity/*.test.js'
|
||||
: '--testNamePattern="[$PLATFORM] tap" -l trace e2e/sanity/*.test.js';
|
||||
|
||||
await run();
|
||||
|
||||
expect(cliCall().fullCommand).toMatch(/\bDETOX_LOGLEVEL="trace" /);
|
||||
expect(cliCall().argv.slice(-3)).toEqual(['--testNamePattern', '[ios] tap', 'e2e/sanity/*.test.js']);
|
||||
expect(logger().warn).toHaveBeenCalledWith(expect.stringContaining('$DETOX_ARGV_OVERRIDE is detected'));
|
||||
});
|
||||
|
||||
// Helpers
|
||||
@@ -533,15 +439,15 @@ describe('CLI', () => {
|
||||
fs.writeFileSync(tempFilePath, content);
|
||||
}
|
||||
|
||||
temporaryFiles.push(tempFilePath);
|
||||
_temporaryFiles.push(tempFilePath);
|
||||
return tempFilePath;
|
||||
}
|
||||
|
||||
async function runRaw(command = '') {
|
||||
let argv;
|
||||
async function runRaw(...command) {
|
||||
let memArgv;
|
||||
|
||||
try {
|
||||
argv = process.argv.splice(2, Infinity, ...command.trim().split(' '));
|
||||
memArgv = process.argv.splice(2, Infinity, ...command);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const testCommand = require('./test');
|
||||
@@ -572,27 +478,36 @@ describe('CLI', () => {
|
||||
parser.parse(command, err => err && reject(err));
|
||||
});
|
||||
} finally {
|
||||
argv && process.argv.splice(2, Infinity, ...argv);
|
||||
memArgv && process.argv.splice(2, Infinity, ...memArgv);
|
||||
}
|
||||
}
|
||||
|
||||
async function run(command = '') {
|
||||
async function run(...args) {
|
||||
detoxConfigPath = tempfile('.json', JSON.stringify(detoxConfig));
|
||||
const __configPath = Math.random() > 0.5 ? '-C' : '--config-path';
|
||||
return runRaw(`test ${__configPath} ${detoxConfigPath} ${command}`);
|
||||
return runRaw('test', __configPath, detoxConfigPath, ...args);
|
||||
}
|
||||
|
||||
function cliCall(index = 0) {
|
||||
const mockCall = cp.execSync.mock.calls[index];
|
||||
if (!_cliCallDump) {
|
||||
_cliCallDump = fs.readFileSync(process.env.CLI_TEST_STDOUT, 'utf8')
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map(line => JSON.parse(line));
|
||||
}
|
||||
|
||||
const mockCall = _cliCallDump[index];
|
||||
if (!mockCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [command, opts] = mockCall;
|
||||
|
||||
return {
|
||||
command,
|
||||
env: _.omitBy(opts.env, (_value, key) => key in process.env),
|
||||
...mockCall,
|
||||
fullCommand: _.chain(logger().log.mock.calls)
|
||||
.filter(([_level, _childMeta, meta]) => meta && meta.env)
|
||||
.get(index)
|
||||
.get(3)
|
||||
.value(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -600,21 +515,7 @@ describe('CLI', () => {
|
||||
return Object.values(detoxConfig.configurations)[0];
|
||||
}
|
||||
|
||||
function isInCMD() {
|
||||
return process.platform === 'win32' && !process.env.SHELL;
|
||||
}
|
||||
|
||||
function quote(s, q = isInCMD() ? `"` : `'`) {
|
||||
return q + s + q;
|
||||
}
|
||||
|
||||
function aCustomDriverModule() {
|
||||
return `
|
||||
class RuntimeDriverClass {};
|
||||
class DeviceAllocationDriverClass {};
|
||||
class DeviceDeallocationDriverClass {};
|
||||
class ExpectClass {};
|
||||
module.exports = { RuntimeDriverClass, DeviceAllocationDriverClass, DeviceDeallocationDriverClass, ExpectClass }
|
||||
`;
|
||||
function mockExitCode(code) {
|
||||
process.env.CLI_EXIT_CODE = code;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
const cp = require('child_process');
|
||||
|
||||
const _ = require('lodash');
|
||||
const parser = require('yargs-parser');
|
||||
const unparse = require('yargs-unparser');
|
||||
|
||||
const detox = require('../../internals');
|
||||
const log = detox.log.child({ cat: ['lifecycle', 'cli'] });
|
||||
const { DetoxRuntimeError } = require('../../src/errors');
|
||||
const { printEnvironmentVariables, prependNodeModulesBinToPATH } = require('../../src/utils/envUtils');
|
||||
const { escapeSpaces } = require('../../src/utils/shellUtils');
|
||||
|
||||
class TestRunnerCommand {
|
||||
constructor() {
|
||||
this._argv = {};
|
||||
this._env = {};
|
||||
this._envHint = {};
|
||||
this._retries = 0;
|
||||
/** @type {Detox.DetoxDeviceConfig} */
|
||||
this._deviceConfig = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Detox.DetoxDeviceConfig} config
|
||||
* @returns {this}
|
||||
*/
|
||||
setDeviceConfig(config) {
|
||||
this._deviceConfig = config;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Detox.DetoxTestRunnerConfig} config
|
||||
* @returns {this}
|
||||
*/
|
||||
setRunnerConfig(config) {
|
||||
this._argv = config.args;
|
||||
this._retries = config.retries;
|
||||
|
||||
if (config.inspectBrk === true) {
|
||||
this._env = this._envHint;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Partial<Readonly<DetoxInternals.CLIConfig>>} cliConfig
|
||||
* @returns {this}
|
||||
*/
|
||||
replicateCLIConfig(cliConfig) {
|
||||
this._envHint = _.omitBy({
|
||||
DETOX_APP_LAUNCH_ARGS: cliConfig.appLaunchArgs,
|
||||
DETOX_ARTIFACTS_LOCATION: cliConfig.artifactsLocation,
|
||||
DETOX_CAPTURE_VIEW_HIERARCHY: cliConfig.captureViewHierarchy,
|
||||
DETOX_CLEANUP: cliConfig.cleanup,
|
||||
DETOX_CONFIGURATION: cliConfig.configuration,
|
||||
DETOX_CONFIG_PATH: cliConfig.configPath,
|
||||
DETOX_DEBUG_SYNCHRONIZATION: cliConfig.debugSynchronization,
|
||||
DETOX_DEVICE_BOOT_ARGS: cliConfig.deviceBootArgs,
|
||||
DETOX_DEVICE_NAME: cliConfig.deviceName,
|
||||
DETOX_FORCE_ADB_INSTALL: this._deviceConfig.type.startsWith('android.')
|
||||
? cliConfig.forceAdbInstall
|
||||
: undefined,
|
||||
DETOX_GPU: cliConfig.gpu,
|
||||
DETOX_HEADLESS: cliConfig.headless,
|
||||
DETOX_KEEP_LOCKFILE: cliConfig.keepLockFile,
|
||||
DETOX_LOGLEVEL: cliConfig.loglevel,
|
||||
DETOX_READ_ONLY_EMU: cliConfig.readonlyEmu,
|
||||
DETOX_RECORD_LOGS: cliConfig.recordLogs,
|
||||
DETOX_RECORD_PERFORMANCE: cliConfig.recordPerformance,
|
||||
DETOX_RECORD_VIDEOS: cliConfig.recordVideos,
|
||||
DETOX_REPORT_SPECS: cliConfig.jestReportSpecs,
|
||||
DETOX_RETRIES: cliConfig.retries,
|
||||
DETOX_REUSE: cliConfig.reuse,
|
||||
DETOX_TAKE_SCREENSHOTS: cliConfig.takeScreenshots,
|
||||
DETOX_USE_CUSTOM_LOGGER: cliConfig.useCustomLogger,
|
||||
}, _.isUndefined);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
let runsLeft = 1 + this._retries;
|
||||
let launchError;
|
||||
|
||||
do {
|
||||
try {
|
||||
if (launchError) {
|
||||
const list = this._argv._.map((file, index) => ` ${index + 1}. ${file}`).join('\n');
|
||||
log.error(
|
||||
`There were failing tests in the following files:\n${list}\n\n` +
|
||||
'Detox CLI is going to restart the test runner with those files...\n'
|
||||
);
|
||||
}
|
||||
|
||||
await this._doExecute();
|
||||
launchError = null;
|
||||
} catch (e) {
|
||||
launchError = e;
|
||||
|
||||
const { failedTestFiles, testFilesToRetry } = detox.session;
|
||||
if (!_.isEmpty(failedTestFiles) || _.isEmpty(testFilesToRetry)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (--runsLeft > 0) {
|
||||
this._argv._ = testFilesToRetry.splice(0, Infinity);
|
||||
// @ts-ignore
|
||||
detox.session.testSessionIndex++; // it is always a primary context, so we can update it
|
||||
}
|
||||
}
|
||||
} while (launchError && runsLeft > 0);
|
||||
|
||||
if (launchError) {
|
||||
throw launchError;
|
||||
}
|
||||
}
|
||||
|
||||
async _doExecute() {
|
||||
const fullCommand = this._buildSpawnArguments().map(escapeSpaces);
|
||||
const fullCommandWithHint = printEnvironmentVariables(this._envHint) + fullCommand.join(' ');
|
||||
|
||||
log.info({ env: this._envHint }, fullCommandWithHint);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
cp.spawn(fullCommand[0], fullCommand.slice(1), {
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
env: _({})
|
||||
.assign(process.env)
|
||||
.assign(this._env)
|
||||
.omitBy(_.isUndefined)
|
||||
.tap(prependNodeModulesBinToPATH)
|
||||
.value()
|
||||
})
|
||||
.on('error', (err) => reject(err))
|
||||
.on('exit', (code) => code === 0
|
||||
? resolve()
|
||||
: reject(new DetoxRuntimeError(`Command failed with exit code = ${code}:\n${fullCommandWithHint}`)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
_buildSpawnArguments() {
|
||||
const { _: specs = [], '--': passthrough = [], $0, ...argv } = this._argv;
|
||||
const { _: $0_, ...$0argv } = parser($0);
|
||||
|
||||
return [
|
||||
...$0_,
|
||||
...unparse($0argv),
|
||||
...unparse(argv),
|
||||
...unparse({ _: [...passthrough, ...specs] }),
|
||||
].map(String);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TestRunnerCommand;
|
||||
@@ -10,27 +10,17 @@ module.exports = {
|
||||
describe:
|
||||
'Select a device configuration from your defined configurations, if not supplied, and there\'s only one configuration, detox will default to it',
|
||||
},
|
||||
o: {
|
||||
alias: 'runner-config',
|
||||
group: 'Configuration:',
|
||||
describe: 'Test runner config file, default value is: e2e/config.json',
|
||||
},
|
||||
l: {
|
||||
alias: 'loglevel',
|
||||
group: 'Debugging:',
|
||||
choices: ['fatal', 'error', 'warn', 'info', 'verbose', 'trace'],
|
||||
choices: ['fatal', 'error', 'warn', 'info', 'verbose', 'debug', 'trace'],
|
||||
describe: 'Log level',
|
||||
},
|
||||
'no-color': {
|
||||
describe: 'Disable colors in log output',
|
||||
boolean: true,
|
||||
},
|
||||
R: {
|
||||
alias: 'retries',
|
||||
group: 'Execution:',
|
||||
describe: '[Jest Circus Only] Re-spawn the test runner for individual failing suite files until they pass, or <N> times at least.',
|
||||
describe: 'Re-spawn the test runner for individual failing suite files until they pass, or <N> times at least.',
|
||||
number: true,
|
||||
default: 0,
|
||||
},
|
||||
r: {
|
||||
alias: 'reuse',
|
||||
@@ -48,10 +38,6 @@ module.exports = {
|
||||
alias: 'debug-synchronization',
|
||||
group: 'Debugging:',
|
||||
coerce(value) {
|
||||
if (value == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (value === false || value === 'false') {
|
||||
return 0;
|
||||
}
|
||||
@@ -96,33 +82,23 @@ module.exports = {
|
||||
choices: ['enabled', 'disabled'],
|
||||
describe: '[iOS Only] Capture *.uihierarchy snapshots on view action errors and device.captureViewHierarchy() calls.',
|
||||
},
|
||||
'record-timeline': {
|
||||
group: 'Debugging:',
|
||||
choices: ['all', 'none'],
|
||||
describe: '[Jest Only] Record tests and events timeline, for visual display on the chrome://tracing tool.',
|
||||
},
|
||||
w: {
|
||||
alias: 'workers',
|
||||
group: 'Execution:',
|
||||
describe: 'Specifies the number of workers the test runner should spawn. Requires a test runner with parallel execution support (e.g. Jest)',
|
||||
number: true,
|
||||
},
|
||||
'jest-report-specs': {
|
||||
group: 'Execution:',
|
||||
describe: '[Jest Only] Whether to output logs per each running spec, in real-time. By default, disabled with multiple workers.',
|
||||
describe: 'Whether to output logs per each running spec, in real-time. By default, disabled with multiple workers.',
|
||||
boolean: true,
|
||||
},
|
||||
H: {
|
||||
alias: 'headless',
|
||||
group: 'Execution:',
|
||||
describe: '[Android Only] Launch emulator in headless mode. Useful when running on CI. Only applicable for Google emulators.',
|
||||
describe: 'Launch device in headless mode. Useful when running on CI.',
|
||||
boolean: true,
|
||||
},
|
||||
gpu: {
|
||||
group: 'Execution:',
|
||||
describe: '[Android Only] Launch emulator with the specific -gpu [gpu mode] parameter. Only applicable for Google emulators.',
|
||||
choices: ['auto', 'host', 'swiftshader_indirect', 'angle_indirect', 'guest', 'off'],
|
||||
describe: '[Android Only] Launch emulator with the specific -gpu [gpu mode] parameter.',
|
||||
},
|
||||
keepLockFile:{
|
||||
keepLockFile: {
|
||||
group: 'Configuration:',
|
||||
describe:'Keep the device lock file when running Detox tests',
|
||||
boolean: true,
|
||||
@@ -143,18 +119,17 @@ module.exports = {
|
||||
},
|
||||
'use-custom-logger': {
|
||||
boolean: true,
|
||||
default: true,
|
||||
group: 'Execution:',
|
||||
describe: `Use Detox' custom console-logging implementation, for logging Detox (non-device) logs. Disabling will fallback to node.js / test runner's implementation (e.g. Jest).`,
|
||||
},
|
||||
'force-adb-install': {
|
||||
boolean: true,
|
||||
group: 'Execution:',
|
||||
describe: `Due to problems with the "adb install" command on Android, Detox resorts to a different scheme for install APK's. Setting true will disable that and force usage of "adb install", instead.`,
|
||||
describe: `[Android Only] Due to problems with the "adb install" command on Android, Detox resorts to a different scheme for install APK's. Setting true will disable that and force usage of "adb install", instead.`,
|
||||
},
|
||||
'inspect-brk': {
|
||||
group: 'Debugging:',
|
||||
describe: 'Allows debugging of the underlying test runner',
|
||||
describe: '[Jest Only] Allows debugging of the underlying test runner',
|
||||
boolean: true,
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const { log } = require('../../internals');
|
||||
const { getJestBooleanArgs } = require('../utils/jestInternals');
|
||||
const { simpleUnquote, extractKnownKeys, disengageBooleanArgs } = require('../utils/yargsUtils');
|
||||
|
||||
const testCommandArgs = require('./builder');
|
||||
const { DETOX_ARGV_OVERRIDE_NOTICE, DEVICE_LAUNCH_ARGS_DEPRECATION } = require('./warnings');
|
||||
|
||||
function applyEnvironmentVariableAddendum(argv, yargs) {
|
||||
if (process.env.DETOX_ARGV_OVERRIDE) {
|
||||
log.warn(DETOX_ARGV_OVERRIDE_NOTICE);
|
||||
|
||||
const { _: positional, '--': passthrough, ...named } = yargs.parse(process.env.DETOX_ARGV_OVERRIDE);
|
||||
|
||||
if (positional) {
|
||||
argv._ = argv._ || [];
|
||||
argv._.push(...positional.map(simpleUnquote));
|
||||
}
|
||||
|
||||
if (passthrough) {
|
||||
argv['--'] = argv['--'] || [];
|
||||
argv['--'].push(...passthrough.map(simpleUnquote));
|
||||
}
|
||||
|
||||
Object.assign(argv, named);
|
||||
}
|
||||
|
||||
return argv;
|
||||
}
|
||||
|
||||
function warnDeviceAppLaunchArgsDeprecation(argv) {
|
||||
if (argv['device-boot-args'] && process.argv.some(a => a.startsWith('--device-launch-args'))) {
|
||||
log.warn(DEVICE_LAUNCH_ARGS_DEPRECATION);
|
||||
}
|
||||
|
||||
return argv;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, *>} argv
|
||||
* @returns {{
|
||||
* detoxArgs: Record<string, *>,
|
||||
* runnerArgs: Record<string, *>
|
||||
* }}
|
||||
*/
|
||||
function splitArgv(argv) {
|
||||
const aliases = extractKnownKeys(testCommandArgs);
|
||||
const isDetoxArg = (_value, key) => key === '$0' || aliases.has(key);
|
||||
|
||||
const detoxArgs = _.pickBy(argv, isDetoxArg);
|
||||
const runnerArgv = _.omitBy(argv, isDetoxArg);
|
||||
runnerArgv._ = runnerArgv._.slice(1); // omit 'test' string, as in 'detox test'
|
||||
if (typeof detoxArgs['debug-synchronization'] === 'string') {
|
||||
const erroneousPassthrough = detoxArgs['debug-synchronization'];
|
||||
detoxArgs['debug-synchronization'] = 3000;
|
||||
runnerArgv._.unshift(erroneousPassthrough);
|
||||
}
|
||||
|
||||
const runnerArgs = disengageBooleanArgs(runnerArgv, getJestBooleanArgs());
|
||||
return { detoxArgs, runnerArgs };
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
module.exports = {
|
||||
applyEnvironmentVariableAddendum,
|
||||
warnDeviceAppLaunchArgsDeprecation,
|
||||
splitArgv,
|
||||
};
|
||||
|
||||
module.exports.default = [
|
||||
applyEnvironmentVariableAddendum,
|
||||
warnDeviceAppLaunchArgsDeprecation,
|
||||
splitArgv,
|
||||
];
|
||||
@@ -1,13 +1,4 @@
|
||||
const { DEVICE_LAUNCH_ARGS_DEPRECATION } = require('../../src/configuration/utils/warnings');
|
||||
const log = require('../../src/utils/logger').child({ __filename });
|
||||
|
||||
function coerceDeprecation(option) {
|
||||
return function coerceDeprecationFn(value) {
|
||||
log.warn(`Beware: ${option} will be removed in the next version of Detox.`);
|
||||
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
const DETOX_ARGV_OVERRIDE_NOTICE = `
|
||||
_____ _____ ___________
|
||||
@@ -30,7 +21,6 @@ const DETOX_ARGV_OVERRIDE_NOTICE = `
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
coerceDeprecation,
|
||||
DETOX_ARGV_OVERRIDE_NOTICE,
|
||||
DEVICE_LAUNCH_ARGS_DEPRECATION,
|
||||
};
|
||||
@@ -2,9 +2,12 @@
|
||||
const Module = require('module');
|
||||
const path = require('path');
|
||||
|
||||
const _ = require('lodash');
|
||||
const resolveFrom = require('resolve-from');
|
||||
|
||||
const DetoxRuntimeError = require('../../src/errors/DetoxRuntimeError');
|
||||
const { DetoxRuntimeError } = require('../../src/errors');
|
||||
|
||||
const { extractKnownKeys } = require('./yargsUtils');
|
||||
|
||||
const getNodeModulePaths = (dir) => Module._nodeModulePaths(dir);
|
||||
|
||||
@@ -65,7 +68,15 @@ async function readJestConfig(argv) {
|
||||
return readConfig(argv, process.cwd(), false);
|
||||
}
|
||||
|
||||
function getJestBooleanArgs() {
|
||||
return _(resolveJestCliArgs())
|
||||
.thru(args => args.options)
|
||||
.pickBy(({ type }) => type === 'boolean')
|
||||
.thru(extractKnownKeys)
|
||||
.value();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveJestCliArgs,
|
||||
getJestBooleanArgs,
|
||||
readJestConfig,
|
||||
};
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const { resolveJestCliArgs } = require('./jestInternals');
|
||||
const testCommandArgs = require('./testCommandArgs');
|
||||
|
||||
function extractKnownKeys(yargsBuilder) {
|
||||
return Object.entries(yargsBuilder).reduce(
|
||||
(set, [key, option]) => {
|
||||
if (option.alias) {
|
||||
if (Array.isArray(option.alias)) {
|
||||
for (const value of option.alias) {
|
||||
set.add(value);
|
||||
}
|
||||
} else {
|
||||
set.add(option.alias);
|
||||
}
|
||||
}
|
||||
|
||||
return set.add(key);
|
||||
},
|
||||
new Set()
|
||||
);
|
||||
}
|
||||
|
||||
function disengageBooleanArgs(argv, booleanKeys) {
|
||||
const result = {};
|
||||
const passthrough = [];
|
||||
|
||||
for (const entry of Object.entries(argv)) {
|
||||
const [key, value] = entry;
|
||||
if (key === '_' || key === '--') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const positiveKey = key.startsWith('no-') ? key.slice(3) : key;
|
||||
if (booleanKeys.has(positiveKey) && typeof value !== 'boolean') {
|
||||
result[positiveKey] = key === positiveKey;
|
||||
passthrough.push(value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
specs: passthrough.concat(argv._),
|
||||
passthrough: {
|
||||
_: argv['--'] || [],
|
||||
...result,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getJestBooleanArgs() {
|
||||
return _(resolveJestCliArgs())
|
||||
.thru(args => args.options)
|
||||
.pickBy(({ type }) => type === 'boolean')
|
||||
.thru(extractKnownKeys)
|
||||
.value();
|
||||
}
|
||||
|
||||
function splitDetoxArgv(argv) {
|
||||
const aliases = extractKnownKeys(testCommandArgs);
|
||||
const isDetoxArg = (_value, key) => aliases.has(key);
|
||||
|
||||
const detoxArgs = _.pickBy(argv, isDetoxArg);
|
||||
const runnerArgs = _.omitBy(argv, isDetoxArg);
|
||||
runnerArgs._ = runnerArgs._.slice(1); // omit 'test' string, as in 'detox test'
|
||||
if (typeof detoxArgs['debug-synchronization'] === 'string') {
|
||||
const erroneousPassthrough = detoxArgs['debug-synchronization'];
|
||||
detoxArgs['debug-synchronization'] = 3000;
|
||||
runnerArgs._.unshift(erroneousPassthrough);
|
||||
}
|
||||
|
||||
return { detoxArgs, runnerArgs };
|
||||
}
|
||||
|
||||
function splitJestArgv(argv) {
|
||||
return realiasJestArgv(disengageBooleanArgs(argv, getJestBooleanArgs()));
|
||||
}
|
||||
|
||||
function realiasJestArgv({ specs, passthrough }) {
|
||||
if (passthrough.hasOwnProperty('t')) {
|
||||
passthrough.testNamePattern = passthrough.t;
|
||||
delete passthrough.t;
|
||||
}
|
||||
|
||||
return { specs, passthrough };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detox: splitDetoxArgv,
|
||||
jest: splitJestArgv,
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @param {Record<string, Record<string, *>>} yargsBuilder
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
function extractKnownKeys(yargsBuilder) {
|
||||
return Object.entries(yargsBuilder).reduce(
|
||||
(set, [key, option]) => {
|
||||
if (option.alias) {
|
||||
if (Array.isArray(option.alias)) {
|
||||
for (const value of option.alias) {
|
||||
set.add(value);
|
||||
}
|
||||
} else {
|
||||
set.add(option.alias);
|
||||
}
|
||||
}
|
||||
|
||||
return set.add(key);
|
||||
},
|
||||
new Set()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, *>} argv
|
||||
* @param {Set<string>} booleanKeys
|
||||
* @returns {Record<string, *>}
|
||||
*/
|
||||
function disengageBooleanArgs(argv, booleanKeys) {
|
||||
const result = {};
|
||||
const passthrough = [];
|
||||
|
||||
for (const entry of Object.entries(argv)) {
|
||||
const [key, value] = entry;
|
||||
if (key === '_' || key === '--') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const positiveKey = key.startsWith('no-') ? key.slice(3) : key;
|
||||
if (booleanKeys.has(positiveKey) && typeof value !== 'boolean') {
|
||||
result[positiveKey] = key === positiveKey;
|
||||
passthrough.push(value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
'_': passthrough.concat(argv._),
|
||||
'--': argv['--'] || [],
|
||||
};
|
||||
}
|
||||
|
||||
function simpleUnquote(arg) {
|
||||
if ((arg[0] === '"' || arg[0] === "'") && arg[0] === arg[arg.length - 1]) {
|
||||
return arg.slice(1, -1);
|
||||
}
|
||||
|
||||
return arg;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
disengageBooleanArgs,
|
||||
extractKnownKeys,
|
||||
simpleUnquote,
|
||||
};
|
||||
+22
-12
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "detox",
|
||||
"description": "E2E tests and automation for mobile",
|
||||
"version": "19.9.1",
|
||||
"version": "20.0.6-breaking.new-global-lifecycle.0",
|
||||
"bin": {
|
||||
"detox": "local-cli/cli.js"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "https://github.com/wix/Detox/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wix/Detox#readme",
|
||||
"main": "./src/index.js",
|
||||
"main": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
"author": "Tal Kol <[email protected]>",
|
||||
"license": "MIT",
|
||||
@@ -32,9 +32,12 @@
|
||||
"postinstall": "node scripts/postinstall.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bunyan": "^1.8.8",
|
||||
"@types/child-process-promise": "^2.2.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/node": "^12.20.37",
|
||||
"@types/node-ipc": "^9.2.0",
|
||||
"@types/ws": "^7.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
@@ -44,7 +47,6 @@
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-unicorn": "^39.0.0",
|
||||
"jest": "^28.0.0",
|
||||
"mocha": ">=6.0.0",
|
||||
"mockdate": "^2.0.1",
|
||||
"prettier": "1.7.0",
|
||||
"react-native": "0.68.2",
|
||||
@@ -55,15 +57,21 @@
|
||||
"dependencies": {
|
||||
"ajv": "^8.6.3",
|
||||
"bunyan": "^1.8.12",
|
||||
"bunyan-debug-stream": "^2.0.1",
|
||||
"bunyan-debug-stream": "^3.1.0",
|
||||
"chalk": "^2.4.2",
|
||||
"child-process-promise": "^2.2.0",
|
||||
"duplexify": "^4.1.2",
|
||||
"find-up": "^4.1.0",
|
||||
"fs-extra": "^4.0.2",
|
||||
"funpermaproxy": "^1.0.1",
|
||||
"glob": "^8.0.3",
|
||||
"ini": "^1.3.4",
|
||||
"json-cycle": "^1.3.0",
|
||||
"lodash": "^4.17.5",
|
||||
"minimist": "^1.2.0",
|
||||
"multi-sort-stream": "^1.0.3",
|
||||
"multipipe": "^4.0.0",
|
||||
"node-ipc": "^9.2.1",
|
||||
"proper-lockfile": "^3.0.2",
|
||||
"resolve-from": "^5.0.0",
|
||||
"sanitize-filename": "^1.6.1",
|
||||
@@ -71,13 +79,16 @@
|
||||
"serialize-error": "^8.0.1",
|
||||
"shell-quote": "^1.7.2",
|
||||
"signal-exit": "^3.0.3",
|
||||
"stream-json": "^1.7.4",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"tail": "^2.0.0",
|
||||
"telnet-client": "1.2.8",
|
||||
"tempfile": "^2.0.0",
|
||||
"trace-event-lib": "^1.3.1",
|
||||
"which": "^1.3.1",
|
||||
"ws": "^7.0.0",
|
||||
"yargs": "^16.0.3",
|
||||
"yargs-parser": "^20.2.2",
|
||||
"yargs-parser": "^20.2.9",
|
||||
"yargs-unparser": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -99,9 +110,9 @@
|
||||
"testRunner": "jest-circus/runner",
|
||||
"roots": [
|
||||
"node_modules",
|
||||
"local-cli",
|
||||
"src",
|
||||
"runners",
|
||||
"local-cli"
|
||||
"runners"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
@@ -141,7 +152,6 @@
|
||||
"src/validation/EnvironmentValidatorBase.js",
|
||||
"src/validation/factories",
|
||||
"src/validation/ios/IosSimulatorEnvValidator",
|
||||
"src/utils/MissingDetox.js",
|
||||
"src/utils/appdatapath.js",
|
||||
"src/utils/debug.js",
|
||||
"src/utils/environment.js",
|
||||
@@ -167,10 +177,10 @@
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"statements": 100,
|
||||
"branches": 100,
|
||||
"functions": 100,
|
||||
"lines": 100
|
||||
"statements": 0,
|
||||
"branches": 0,
|
||||
"functions": 0,
|
||||
"lines": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`file migration should work for: jest-circus/environment 1`] = `
|
||||
"[DEPRECATION] Detox detected an attempt to require a module from an outdated location, please change in your project:
|
||||
- detox/runners/jest-circus/environment
|
||||
+ detox/runners/jest/environment
|
||||
+ detox/runners/jest/testEnvironment
|
||||
"
|
||||
`;
|
||||
|
||||
|
||||
@@ -7,15 +7,15 @@ describe('file migration', () => {
|
||||
});
|
||||
|
||||
test.each([
|
||||
['environment'],
|
||||
['environment', 'testEnvironment'],
|
||||
['index'],
|
||||
['reporter'],
|
||||
])('should work for: jest-circus/%s', (moduleName) => {
|
||||
const newImpl = require(`../jest/${moduleName}`);
|
||||
])('should work for: jest-circus/%s', (oldModuleName, newModuleName = oldModuleName) => {
|
||||
const newImpl = require(`../jest/${newModuleName}`);
|
||||
expect(logger).not.toHaveBeenCalled();
|
||||
expect(require(`../jest-circus/${moduleName}`)).toBe(newImpl);
|
||||
expect(require(`../jest-circus/${oldModuleName}`)).toBe(newImpl);
|
||||
expect(logger.mock.calls[0][0]).toMatchSnapshot();
|
||||
expect(require(`../jest-circus/${moduleName}`)).toBe(newImpl);
|
||||
expect(require(`../jest-circus/${oldModuleName}`)).toBe(newImpl);
|
||||
expect(logger).not.toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
module.exports = {
|
||||
lifecycle: {
|
||||
onRunStart: Symbol('run_start'),
|
||||
onRunDescribeStart: Symbol('run_describe_start'),
|
||||
onTestStart: Symbol('test_start'),
|
||||
onHookStart: Symbol('hook_start'),
|
||||
onHookFailure: Symbol('hook_failure'),
|
||||
onHookSuccess: Symbol('hook_success'),
|
||||
onTestFnStart: Symbol('test_fn_start'),
|
||||
onTestFnFailure: Symbol('test_fn_failure'),
|
||||
onTestFnSuccess: Symbol('test_fn_success'),
|
||||
onTestDone: Symbol('test_done'),
|
||||
onRunDescribeFinish: Symbol('run_describe_finish'),
|
||||
onRunFinish: Symbol('run_finish'),
|
||||
},
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require('../migration')(__filename);
|
||||
@@ -0,0 +1,6 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = require('../../migration')(
|
||||
__filename,
|
||||
path.join(__dirname, '../../jest/testEnvironment/index.js')
|
||||
);
|
||||
@@ -1,46 +1,25 @@
|
||||
const chalk = require('chalk');
|
||||
|
||||
// @ts-ignore
|
||||
console.error(chalk.yellow(`
|
||||
========================= THE NEW JOURNEY BEGINS =============================
|
||||
|
||||
https://github.com/wix/Detox/blob/master/docs/Guide.Jest.md
|
||||
https://wix.github.io/Detox/docs/guide/jest
|
||||
|
||||
_.-;-._ Sorry, traveler from the lands of Detox 19!
|
||||
;_.JL___;
|
||||
F"-/\\_-7L Detox 20 comes without old adapters for Jest.
|
||||
| a/ e | \\ You have to rearrange your init code before
|
||||
,L,c;,.='/;, you can continue your journey.
|
||||
_,-;;S:;:S;;:' '--._
|
||||
;. \\;;s:::s;;: .' /\\ Navigate to the link above and follow the
|
||||
/ \\ ;::::;; / / \\ migration guide steps.
|
||||
/ , k ;S';;'S.' j __,l
|
||||
,---/| / /S /S '. |' ; Sincerely yours,
|
||||
,Ljjj |/|.' s .' s \\ L | Detox team.
|
||||
LL,_ ]( \\ / '. '.|| ;
|
||||
||\\ > / ;-.'_.-.___\\.-'(|=="(
|
||||
JJ," / |_ [ ] _]| /
|
||||
LL\\/ ,' '--'-'-----' \\ (
|
||||
|| ; | | >
|
||||
JJ | |\\ |,/
|
||||
LL | || ' |
|
||||
|| | || . |
|
||||
JJ /_ || ;_|
|
||||
LL L "==='|i======='_|
|
||||
|| i----' '-------';
|
||||
JJ ';-----.------,-'
|
||||
LL L_.__J,'---;'
|
||||
|| | ,| (
|
||||
JJ .'= (| ,_|
|
||||
LL / .'L_ \\
|
||||
snd || '---' '.___>
|
||||
Credit: "Gimli" by Shanaka Dias
|
||||
Sorry to say that Detox 20 comes without old adapters for Jest.
|
||||
You have to rearrange your init code before you can continue your journey.
|
||||
|
||||
https://github.com/wix/Detox/blob/master/docs/Guide.Jest.md
|
||||
Navigate to the link and follow the migration guide steps.
|
||||
|
||||
Sincerely yours,
|
||||
Detox team.
|
||||
|
||||
https://wix.github.io/Detox/docs/guide/jest
|
||||
|
||||
========================= THE NEW JOURNEY BEGINS =============================
|
||||
|
||||
`));
|
||||
|
||||
throw Object.assign(new Error(
|
||||
'\n\nPlease follow the new Jest setup guide:\nhttps://github.com/wix/Detox/blob/master/docs/Guide.Jest.md\n\n'
|
||||
'\n\nPlease follow the new Jest setup guide:\nhttps://wix.github.io/Detox/docs/guide/jest\n\n'
|
||||
), { stack: '' });
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
// @ts-nocheck
|
||||
const maybeNodeEnvironment = require('jest-environment-node'); // eslint-disable-line node/no-extraneous-require
|
||||
const NodeEnvironment = maybeNodeEnvironment.default || maybeNodeEnvironment;
|
||||
|
||||
const DetoxError = require('../../src/errors/DetoxError');
|
||||
const Timer = require('../../src/utils/Timer');
|
||||
|
||||
const DetoxCoreListener = require('./listeners/DetoxCoreListener');
|
||||
const DetoxInitErrorListener = require('./listeners/DetoxInitErrorListener');
|
||||
const assertExistingContext = require('./utils/assertExistingContext');
|
||||
const { assertJestCircus27 } = require('./utils/assertJestCircus27');
|
||||
const wrapErrorWithNoopLifecycle = require('./utils/wrapErrorWithNoopLifecycle');
|
||||
|
||||
const SYNC_CIRCUS_EVENTS = new Set([
|
||||
'start_describe_definition',
|
||||
'finish_describe_definition',
|
||||
'add_hook',
|
||||
'add_test',
|
||||
'error',
|
||||
]);
|
||||
|
||||
/**
|
||||
* @see https://www.npmjs.com/package/jest-circus#overview
|
||||
*/
|
||||
class DetoxCircusEnvironment extends NodeEnvironment {
|
||||
constructor(config, context) {
|
||||
super(assertJestCircus27(config), assertExistingContext(context));
|
||||
|
||||
/** @private */
|
||||
this._timer = null;
|
||||
/** @private */
|
||||
this._listenerFactories = {
|
||||
DetoxInitErrorListener,
|
||||
DetoxCoreListener,
|
||||
};
|
||||
/** @private */
|
||||
this._calledDetoxInit = false;
|
||||
/** @private */
|
||||
this._calledDetoxCleanup = false;
|
||||
/** @protected */
|
||||
this.testPath = context.testPath;
|
||||
/** @protected */
|
||||
this.testEventListeners = [];
|
||||
/** @protected */
|
||||
this.initTimeout = 300000;
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await super.setup();
|
||||
|
||||
this.global.detox = require('../../src')
|
||||
._setGlobal(this.global)
|
||||
._suppressLoggingInitErrors();
|
||||
}
|
||||
|
||||
async teardown() {
|
||||
try {
|
||||
if (this._calledDetoxInit && !this._calledDetoxCleanup) {
|
||||
await this._runEmergencyTeardown();
|
||||
}
|
||||
} finally {
|
||||
await super.teardown();
|
||||
}
|
||||
}
|
||||
|
||||
get detox() {
|
||||
return this.global.detox;
|
||||
}
|
||||
|
||||
async handleTestEvent(event, state) {
|
||||
const { name } = event;
|
||||
|
||||
if (SYNC_CIRCUS_EVENTS.has(name)) {
|
||||
return this._handleTestEventSync(event, state);
|
||||
}
|
||||
|
||||
this._timer = new Timer({
|
||||
description: `handling jest-circus "${name}" event`,
|
||||
timeout: name === 'setup' ? this.initTimeout : state.testTimeout,
|
||||
});
|
||||
|
||||
try {
|
||||
if (name === 'setup') {
|
||||
await this._onSetup(state);
|
||||
}
|
||||
|
||||
for (const listener of this.testEventListeners) {
|
||||
if (typeof listener[name] === 'function') {
|
||||
try {
|
||||
await this._timer.run(() => listener[name](event, state));
|
||||
} catch (listenerError) {
|
||||
this._logError(listenerError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'teardown') {
|
||||
await this._onTeardown(state);
|
||||
}
|
||||
} finally {
|
||||
this._timer.dispose();
|
||||
this._timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_handleTestEventSync(event, state) {
|
||||
const { name } = event;
|
||||
|
||||
for (const listener of this.testEventListeners) {
|
||||
if (typeof listener[name] === 'function') {
|
||||
listener[name](event, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _onSetup(state) {
|
||||
let detox;
|
||||
|
||||
try {
|
||||
detox = await this._timer.run(async () => {
|
||||
try {
|
||||
this._calledDetoxInit = true;
|
||||
return await this.initDetox();
|
||||
} catch (actualError) {
|
||||
state.unhandledErrors.push(actualError);
|
||||
this._logError(actualError);
|
||||
throw actualError;
|
||||
}
|
||||
});
|
||||
} catch (maybeActualError) {
|
||||
if (!state.unhandledErrors.includes(maybeActualError)) {
|
||||
const timeoutError = maybeActualError;
|
||||
state.unhandledErrors.push(timeoutError);
|
||||
this._logError(timeoutError);
|
||||
}
|
||||
|
||||
detox = wrapErrorWithNoopLifecycle(maybeActualError);
|
||||
} finally {
|
||||
this._timer.reset(state.testTimeout);
|
||||
}
|
||||
|
||||
this._instantiateListeners(detox);
|
||||
}
|
||||
|
||||
_instantiateListeners(detoxInstance) {
|
||||
for (const Listener of Object.values(this._listenerFactories)) {
|
||||
this.testEventListeners.push(new Listener({
|
||||
detox: detoxInstance,
|
||||
env: this,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async _onTeardown(state) {
|
||||
try {
|
||||
this._calledDetoxCleanup = true;
|
||||
await this._timer.run(() => this.cleanupDetox());
|
||||
} catch (cleanupError) {
|
||||
state.unhandledErrors.push(cleanupError);
|
||||
this._logError(cleanupError);
|
||||
}
|
||||
}
|
||||
|
||||
async _runEmergencyTeardown() {
|
||||
this._timer = new Timer({
|
||||
description: `handling environment teardown`,
|
||||
timeout: this.initTimeout,
|
||||
});
|
||||
|
||||
try {
|
||||
await this._timer.run(() => this.cleanupDetox());
|
||||
} catch (cleanupError) {
|
||||
this._logError(cleanupError);
|
||||
} finally {
|
||||
this._timer.dispose();
|
||||
this._timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
get _logger() {
|
||||
return require('../../src/utils/logger');
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_logError(e) {
|
||||
this._logger.error(DetoxError.format(e));
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
async initDetox() {
|
||||
return this.detox.init();
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
async cleanupDetox() {
|
||||
return this.detox.cleanup();
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
registerListeners(map) {
|
||||
Object.assign(this._listenerFactories, map);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DetoxCircusEnvironment;
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = async () => require('../../internals').init({ workerId: null });
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = async () => require('../../internals').cleanup();
|
||||
@@ -1,9 +1,21 @@
|
||||
const DetoxCircusEnvironment = require('./environment');
|
||||
const SpecReporter = require('./listeners/SpecReporter');
|
||||
const WorkerAssignReporter = require('./listeners/WorkerAssignReporter');
|
||||
|
||||
module.exports = {
|
||||
DetoxCircusEnvironment,
|
||||
SpecReporter,
|
||||
WorkerAssignReporter,
|
||||
//#region *** Runner Realm ***
|
||||
|
||||
get DetoxCircusEnvironment() {
|
||||
return require('./testEnvironment');
|
||||
},
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region *** Worker Realm ***
|
||||
|
||||
get globalSetup() {
|
||||
return require('./globalSetup');
|
||||
},
|
||||
|
||||
get globalTeardown() {
|
||||
return require('./globalTeardown');
|
||||
},
|
||||
|
||||
//#endregion
|
||||
};
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
// @ts-nocheck
|
||||
const _ = require('lodash');
|
||||
|
||||
const {
|
||||
onRunDescribeStart,
|
||||
onTestStart,
|
||||
onHookFailure,
|
||||
onTestFnFailure,
|
||||
onTestDone,
|
||||
onRunDescribeFinish,
|
||||
} = require('../../integration').lifecycle;
|
||||
const { getFullTestName, hasTimedOut } = require('../utils');
|
||||
|
||||
const RETRY_TIMES = Symbol.for('RETRY_TIMES');
|
||||
|
||||
class DetoxCoreListener {
|
||||
constructor({ detox, env }) {
|
||||
this._startedTests = new WeakSet();
|
||||
this._testsFailedBeforeStart = new WeakSet();
|
||||
this._env = env;
|
||||
this._testRunTimes = 1;
|
||||
this.detox = detox;
|
||||
}
|
||||
|
||||
_getTestInvocations(test) {
|
||||
const { DETOX_RERUN_INDEX } = process.env;
|
||||
|
||||
if (!isNaN(DETOX_RERUN_INDEX)) {
|
||||
return Number(DETOX_RERUN_INDEX) * this._testRunTimes + test.invocations;
|
||||
} else {
|
||||
return test.invocations;
|
||||
}
|
||||
}
|
||||
|
||||
async run_describe_start({ describeBlock: { name, children } }) {
|
||||
if (children.length) {
|
||||
await this.detox[onRunDescribeStart]({ name });
|
||||
}
|
||||
}
|
||||
|
||||
async run_describe_finish({ describeBlock: { name, children } }) {
|
||||
if (children.length) {
|
||||
await this.detox[onRunDescribeFinish]({ name });
|
||||
}
|
||||
}
|
||||
|
||||
async test_start({ test }) {
|
||||
if (!_.isEmpty(test.errors)) {
|
||||
this._testsFailedBeforeStart.add(test);
|
||||
}
|
||||
|
||||
const circusRetryTimes = +this._env.global[RETRY_TIMES];
|
||||
this._testRunTimes = isNaN(circusRetryTimes) ? 1 : 1 + circusRetryTimes;
|
||||
}
|
||||
|
||||
async hook_start(_event, state) {
|
||||
await this._onBeforeActualTestStart(state.currentlyRunningTest);
|
||||
}
|
||||
|
||||
async hook_failure({ error, hook }) {
|
||||
await this.detox[onHookFailure]({
|
||||
error,
|
||||
hook: hook.type,
|
||||
});
|
||||
}
|
||||
|
||||
async test_fn_start({ test }) {
|
||||
await this._onBeforeActualTestStart(test);
|
||||
}
|
||||
|
||||
async test_fn_failure({ error }) {
|
||||
await this.detox[onTestFnFailure]({ error });
|
||||
}
|
||||
|
||||
async _onBeforeActualTestStart(test) {
|
||||
if (!test || test.status === 'skip' || this._startedTests.has(test) || this._testsFailedBeforeStart.has(test)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._startedTests.add(test);
|
||||
|
||||
await this.detox[onTestStart]({
|
||||
title: test.name,
|
||||
fullName: getFullTestName(test),
|
||||
status: 'running',
|
||||
invocations: this._getTestInvocations(test),
|
||||
});
|
||||
}
|
||||
|
||||
async test_done({ test }) {
|
||||
if (this._startedTests.has(test)) {
|
||||
await this.detox[onTestDone]({
|
||||
title: test.name,
|
||||
fullName: getFullTestName(test),
|
||||
status: test.errors.length ? 'failed' : 'passed',
|
||||
invocations: this._getTestInvocations(test),
|
||||
timedOut: hasTimedOut(test)
|
||||
});
|
||||
|
||||
this._startedTests.delete(test);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DetoxCoreListener;
|
||||
@@ -1,33 +0,0 @@
|
||||
const path = require('path');
|
||||
|
||||
const chalk = require('chalk').default;
|
||||
const _ = require('lodash');
|
||||
|
||||
const log = require('../../../src/utils/logger').child();
|
||||
|
||||
class WorkerAssignReporter {
|
||||
constructor({ detox, env }) {
|
||||
this._detox = detox;
|
||||
this._env = env;
|
||||
}
|
||||
|
||||
run_start() {
|
||||
log.info({ event: 'WORKER_ASSIGN' }, `${this._formatTestName()} is assigned to ${this._formatDeviceName()}`);
|
||||
}
|
||||
|
||||
_formatDeviceName() {
|
||||
const deviceName = _.attempt(() => this._detox.device.name);
|
||||
const formattedDeviceName = _.isError(deviceName)
|
||||
? chalk.redBright('undefined')
|
||||
: chalk.blueBright(deviceName);
|
||||
|
||||
return formattedDeviceName;
|
||||
}
|
||||
|
||||
_formatTestName() {
|
||||
const testName = path.basename(this._env.testPath);
|
||||
return chalk.whiteBright(testName);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WorkerAssignReporter;
|
||||
@@ -1,18 +1,5 @@
|
||||
const { VerboseReporter: JestVerboseReporter } = require('@jest/reporters'); // eslint-disable-line node/no-extraneous-require
|
||||
|
||||
const FailingTestsReporter = require('./FailingTestsReporter');
|
||||
|
||||
class DetoxReporter extends JestVerboseReporter {
|
||||
constructor(globalConfig) {
|
||||
super(globalConfig);
|
||||
this._failingTestsReporter = new FailingTestsReporter();
|
||||
}
|
||||
|
||||
async onRunComplete(contexts, results) {
|
||||
// @ts-ignore
|
||||
await super.onRunComplete(contexts, results);
|
||||
await this._failingTestsReporter.onRunComplete(contexts, results);
|
||||
}
|
||||
}
|
||||
class DetoxReporter extends JestVerboseReporter {}
|
||||
|
||||
module.exports = DetoxReporter;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
const path = require('path');
|
||||
|
||||
const { saveLastFailedTests } = require('../../../src/utils/lastFailedTests');
|
||||
|
||||
class FailingTestsReporter {
|
||||
async onRunComplete(_contexts, { testResults }) {
|
||||
const cwd = process.cwd();
|
||||
const failedFiles = testResults
|
||||
.filter(result => result.numFailingTests > 0)
|
||||
.map(result => path.relative(cwd, result.testFilePath));
|
||||
|
||||
await saveLastFailedTests(failedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FailingTestsReporter;
|
||||
@@ -0,0 +1,173 @@
|
||||
const resolveFrom = require('resolve-from');
|
||||
const maybeNodeEnvironment = require(resolveFrom(process.cwd(), 'jest-environment-node'));
|
||||
// @ts-ignore
|
||||
const NodeEnvironment = maybeNodeEnvironment.default || maybeNodeEnvironment;
|
||||
|
||||
const detox = require('../../../internals');
|
||||
const Timer = require('../../../src/utils/Timer');
|
||||
|
||||
const {
|
||||
DetoxCoreListener,
|
||||
DetoxInitErrorListener,
|
||||
DetoxPlatformFilterListener,
|
||||
SpecReporter,
|
||||
WorkerAssignReporter
|
||||
} = require('./listeners');
|
||||
const assertExistingContext = require('./utils/assertExistingContext');
|
||||
const { assertJestCircus27 } = require('./utils/assertJestCircus27');
|
||||
|
||||
const SYNC_CIRCUS_EVENTS = new Set([
|
||||
'start_describe_definition',
|
||||
'finish_describe_definition',
|
||||
'add_hook',
|
||||
'add_test',
|
||||
'error',
|
||||
]);
|
||||
|
||||
const log = detox.log.child({ cat: 'lifecycle,jest-environment' });
|
||||
|
||||
/**
|
||||
* @see https://www.npmjs.com/package/jest-circus#overview
|
||||
*/
|
||||
class DetoxCircusEnvironment extends NodeEnvironment {
|
||||
constructor(config, context) {
|
||||
super(assertJestCircus27(config), assertExistingContext(context));
|
||||
|
||||
/** @private */
|
||||
this._timer = null;
|
||||
/** @private */
|
||||
this._listenerFactories = {
|
||||
DetoxInitErrorListener,
|
||||
DetoxPlatformFilterListener,
|
||||
DetoxCoreListener,
|
||||
SpecReporter,
|
||||
WorkerAssignReporter,
|
||||
};
|
||||
/** @private */
|
||||
this._shouldManageDetox = detox.getStatus() === 'inactive';
|
||||
/** @internal */
|
||||
this.testPath = context.testPath;
|
||||
/** @protected */
|
||||
this.testEventListeners = [];
|
||||
/** @protected */
|
||||
this.initTimeout = detox.config.testRunner.jest.initTimeout;
|
||||
/** @internal */
|
||||
log.trace.begin(this.testPath);
|
||||
|
||||
this.setup = log.trace.complete.bind(null, 'set up environment', this.setup.bind(this));
|
||||
const _teardown = this.teardown.bind(this);
|
||||
this.teardown = async () => {
|
||||
try {
|
||||
await log.trace.complete('tear down environment', _teardown);
|
||||
} finally {
|
||||
await log.trace.end();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async setup() {
|
||||
await super.setup();
|
||||
await Timer.run({
|
||||
description: `setting up Detox environment`,
|
||||
timeout: this.initTimeout,
|
||||
fn: async () => {
|
||||
await this.initDetox();
|
||||
this._instantiateListeners();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async handleTestEvent(event, state) {
|
||||
const { name } = event;
|
||||
|
||||
if (SYNC_CIRCUS_EVENTS.has(name)) {
|
||||
return this._handleTestEventSync(event, state);
|
||||
}
|
||||
|
||||
this._timer = new Timer({
|
||||
description: `handling jest-circus "${name}" event`,
|
||||
timeout: state.testTimeout != null ? state.testTimeout : this.initTimeout,
|
||||
});
|
||||
|
||||
try {
|
||||
for (const listener of this.testEventListeners) {
|
||||
if (typeof listener[name] !== 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this._timer.run(() => listener[name](event, state));
|
||||
} catch (listenerError) {
|
||||
log.error(listenerError);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this._timer.dispose();
|
||||
this._timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async teardown() {
|
||||
await Timer.run({
|
||||
description: `tearing down Detox environment`,
|
||||
timeout: this.initTimeout,
|
||||
fn: async () => await this.cleanupDetox(),
|
||||
});
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
registerListeners(map) {
|
||||
Object.assign(this._listenerFactories, map);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
async initDetox() {
|
||||
const opts = {
|
||||
global: this.global,
|
||||
workerId: `w${process.env.JEST_WORKER_ID}`,
|
||||
};
|
||||
|
||||
if (this._shouldManageDetox) {
|
||||
await detox.init(opts);
|
||||
} else {
|
||||
await detox.installWorker(opts);
|
||||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
async cleanupDetox() {
|
||||
if (this._shouldManageDetox) {
|
||||
await detox.cleanup();
|
||||
} else {
|
||||
await detox.uninstallWorker();
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_handleTestEventSync(event, state) {
|
||||
const { name } = event;
|
||||
|
||||
for (const listener of this.testEventListeners) {
|
||||
if (typeof listener[name] === 'function') {
|
||||
listener[name](event, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_instantiateListeners() {
|
||||
for (const Listener of Object.values(this._listenerFactories)) {
|
||||
this.testEventListeners.push(new Listener({
|
||||
env: this,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DetoxCircusEnvironment;
|
||||
@@ -0,0 +1,154 @@
|
||||
// @ts-nocheck
|
||||
const _ = require('lodash');
|
||||
|
||||
const detox = require('../../../..');
|
||||
const detoxInternals = require('../../../../internals');
|
||||
const { getFullTestName, hasTimedOut } = require('../utils');
|
||||
|
||||
const RETRY_TIMES = Symbol.for('RETRY_TIMES');
|
||||
|
||||
const log = detoxInternals.log.child({ cat: 'lifecycle,jest-environment' });
|
||||
|
||||
class DetoxCoreListener {
|
||||
constructor({ env }) {
|
||||
this._startedTests = new WeakSet();
|
||||
this._testsFailedBeforeStart = new WeakSet();
|
||||
this._env = env;
|
||||
this._testRunTimes = 1;
|
||||
}
|
||||
|
||||
async setup() {
|
||||
// Workaround to override Jest's expect
|
||||
if (detoxInternals.config.behavior.init.exposeGlobals) {
|
||||
this._env.global.expect = detox.expect;
|
||||
}
|
||||
}
|
||||
|
||||
async run_describe_start({ describeBlock }) {
|
||||
if (describeBlock.children.length) {
|
||||
log.trace.begin(describeBlock.parent ? describeBlock.name : 'run the tests');
|
||||
await detoxInternals.onRunDescribeStart({
|
||||
name: describeBlock.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async run_describe_finish({ describeBlock }) {
|
||||
if (describeBlock.children.length) {
|
||||
await detoxInternals.onRunDescribeFinish({ name: describeBlock.name });
|
||||
log.trace.end();
|
||||
}
|
||||
}
|
||||
|
||||
async test_start({ test }) {
|
||||
if (!_.isEmpty(test.errors)) {
|
||||
this._testsFailedBeforeStart.add(test);
|
||||
}
|
||||
|
||||
const circusRetryTimes = +this._env.global[RETRY_TIMES];
|
||||
this._testRunTimes = isNaN(circusRetryTimes) ? 1 : 1 + circusRetryTimes;
|
||||
}
|
||||
|
||||
async hook_start(event, state) {
|
||||
await this._onBeforeActualTestStart(state.currentlyRunningTest);
|
||||
log.trace.begin({ functionCode: event.hook.fn.toString() }, event.hook.type);
|
||||
}
|
||||
|
||||
async hook_success() {
|
||||
log.trace.end({ success: true });
|
||||
}
|
||||
|
||||
async hook_failure({ error }) {
|
||||
log.trace.end({ success: false, error });
|
||||
}
|
||||
|
||||
async test_fn_start({ test }) {
|
||||
await this._onBeforeActualTestStart(test);
|
||||
log.trace.begin({ functionCode: test.fn.toString() }, 'test_fn');
|
||||
}
|
||||
|
||||
async test_fn_success() {
|
||||
log.trace.end({ success: true });
|
||||
}
|
||||
|
||||
async test_fn_failure({ error }) {
|
||||
await detoxInternals.onTestFnFailure({ error });
|
||||
log.trace.end({ success: false, error });
|
||||
}
|
||||
|
||||
async test_done({ test }) {
|
||||
if (this._startedTests.has(test)) {
|
||||
const failed = test.errors.length > 0;
|
||||
const metadata = {
|
||||
...this._getTestMetadata(test),
|
||||
status: failed ? 'failed' : 'passed',
|
||||
timedOut: failed ? hasTimedOut(test) : undefined,
|
||||
};
|
||||
|
||||
await detoxInternals.onTestDone(metadata);
|
||||
this._startedTests.delete(test);
|
||||
log.trace.end({
|
||||
status: metadata.status,
|
||||
timedOut: metadata.timedOut,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async run_finish(_event, state) {
|
||||
const hasFailedTests = this._hasFailedTests(state.rootDescribeBlock);
|
||||
if (hasFailedTests) {
|
||||
const handledByJestCircus = this._testRunTimes > 1 && !detoxInternals.config.testRunner.jest.retryAfterCircusRetries;
|
||||
await detoxInternals.reportFailedTests([this._env.testPath], handledByJestCircus);
|
||||
}
|
||||
}
|
||||
|
||||
async _onBeforeActualTestStart(test) {
|
||||
if (!test || test.status === 'skip' || this._startedTests.has(test) || this._testsFailedBeforeStart.has(test)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
...this._getTestMetadata(test),
|
||||
status: 'running',
|
||||
};
|
||||
|
||||
this._startedTests.add(test);
|
||||
|
||||
log.trace.begin({
|
||||
context: 'test',
|
||||
status: metadata.status,
|
||||
fullName: metadata.fullName,
|
||||
invocations: metadata.invocations,
|
||||
}, metadata.title);
|
||||
|
||||
await detoxInternals.onTestStart(metadata);
|
||||
return true;
|
||||
}
|
||||
|
||||
_getTestInvocations(test) {
|
||||
const { testSessionIndex } = detoxInternals.session;
|
||||
return testSessionIndex * this._testRunTimes + test.invocations;
|
||||
}
|
||||
|
||||
_hasFailedTests(block) {
|
||||
if (block.children) {
|
||||
for (const child of block.children) {
|
||||
if (this._hasFailedTests(child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return block.errors ? block.errors.length > 0 : false;
|
||||
}
|
||||
|
||||
_getTestMetadata(test) {
|
||||
return {
|
||||
title: test.name,
|
||||
fullName: getFullTestName(test),
|
||||
invocations: this._getTestInvocations(test),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DetoxCoreListener;
|
||||
+1
-1
@@ -15,7 +15,7 @@ class DetoxInitErrorListener {
|
||||
if (!_.isEmpty(unhandledErrors)) {
|
||||
const currentTest = _.last(currentDescribeBlock.children);
|
||||
const dummyError = new Error('Environment setup failed. See the detailed error below.');
|
||||
delete dummyError.stack;
|
||||
dummyError.stack = '';
|
||||
|
||||
currentTest.errors.push(dummyError);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const { device } = require('../../../..');
|
||||
|
||||
const PLATFORM_REGEXP = /^:([^:]+):/;
|
||||
|
||||
class DetoxPlatformFilterListener {
|
||||
constructor() {
|
||||
this._platform = device.getPlatform();
|
||||
}
|
||||
|
||||
start_describe_definition(event, state) {
|
||||
const match = event.blockName.match(PLATFORM_REGEXP);
|
||||
if (match && match[1] !== this._platform) {
|
||||
state.currentDescribeBlock.mode = 'skip';
|
||||
}
|
||||
}
|
||||
|
||||
add_test(event, state) {
|
||||
const match = event.testName.match(PLATFORM_REGEXP);
|
||||
if (match && match[1] !== this._platform) {
|
||||
_.last(state.currentDescribeBlock.children).mode = 'skip';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DetoxPlatformFilterListener;
|
||||
+20
-5
@@ -1,6 +1,6 @@
|
||||
const chalk = require('chalk').default;
|
||||
|
||||
const log = require('../../../src/utils/logger').child();
|
||||
const { config, log, session } = require('../../../../internals');
|
||||
const { traceln } = require('../utils/stdout');
|
||||
|
||||
const RESULT_SKIPPED = chalk.yellow('SKIPPED');
|
||||
@@ -15,7 +15,16 @@ class SpecReporter {
|
||||
this._suitesDesc = '';
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
const jestSection = config.testRunner.jest;
|
||||
const reportSpecs = jestSection && jestSection.reportSpecs;
|
||||
|
||||
return reportSpecs !== undefined ? reportSpecs : session.workersCount === 1;
|
||||
}
|
||||
|
||||
run_describe_start(event) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
if (event.describeBlock.parent !== undefined) {
|
||||
this._onSuiteStart({
|
||||
description: event.describeBlock.name,
|
||||
@@ -24,12 +33,16 @@ class SpecReporter {
|
||||
}
|
||||
|
||||
run_describe_finish(event) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
if (event.describeBlock.parent !== undefined) {
|
||||
this._onSuiteEnd();
|
||||
}
|
||||
}
|
||||
|
||||
test_start(event) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const { test } = event;
|
||||
this._onTestStart({
|
||||
description: test.name,
|
||||
@@ -38,6 +51,8 @@ class SpecReporter {
|
||||
}
|
||||
|
||||
test_done(event) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const { test } = event;
|
||||
const testInfo = {
|
||||
description: test.name,
|
||||
@@ -47,6 +62,8 @@ class SpecReporter {
|
||||
}
|
||||
|
||||
test_skip(event) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const testInfo = {
|
||||
description: event.test.name,
|
||||
};
|
||||
@@ -101,10 +118,8 @@ class SpecReporter {
|
||||
const retriesDescription = (invocations > 1) ? chalk.gray(` [Retry #${invocations - 1}]`) : '';
|
||||
const status = chalk.gray(_status ? ` [${_status}]` : '');
|
||||
const desc = this._suitesDesc + testDescription + retriesDescription + status;
|
||||
log.info({ event: 'SPEC_STATE_CHANGE' }, desc);
|
||||
log.info({ cat: 'lifecycle' }, desc);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = process.env.DETOX_REPORT_SPECS === 'true'
|
||||
? SpecReporter
|
||||
: class {};
|
||||
module.exports = SpecReporter;
|
||||
@@ -0,0 +1,34 @@
|
||||
const path = require('path');
|
||||
|
||||
const chalk = require('chalk').default;
|
||||
const _ = require('lodash');
|
||||
|
||||
const { device } = require('../../../..');
|
||||
const { config, log } = require('../../../../internals');
|
||||
|
||||
class WorkerAssignReporter {
|
||||
constructor({ env }) {
|
||||
this._testName = path.basename(env.testPath);
|
||||
}
|
||||
|
||||
run_start() {
|
||||
if (config.testRunner.jest.reportWorkerAssign) {
|
||||
log.info({ cat: 'lifecycle' }, `${this._formatTestName()} is assigned to ${this._formatDeviceName()}`);
|
||||
}
|
||||
}
|
||||
|
||||
_formatDeviceName() {
|
||||
const deviceName = _.attempt(() => device.name);
|
||||
const formattedDeviceName = _.isError(deviceName)
|
||||
? chalk.redBright('undefined')
|
||||
: chalk.blueBright(deviceName);
|
||||
|
||||
return formattedDeviceName;
|
||||
}
|
||||
|
||||
_formatTestName() {
|
||||
return chalk.whiteBright(this._testName);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WorkerAssignReporter;
|
||||
@@ -0,0 +1,13 @@
|
||||
const DetoxCoreListener = require('./DetoxCoreListener');
|
||||
const DetoxInitErrorListener = require('./DetoxInitErrorListener');
|
||||
const DetoxPlatformFilterListener = require('./DetoxPlatformFilterListener');
|
||||
const SpecReporter = require('./SpecReporter');
|
||||
const WorkerAssignReporter = require('./WorkerAssignReporter');
|
||||
|
||||
module.exports = {
|
||||
DetoxCoreListener,
|
||||
DetoxInitErrorListener,
|
||||
DetoxPlatformFilterListener,
|
||||
SpecReporter,
|
||||
WorkerAssignReporter,
|
||||
};
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
const { DetoxRuntimeError } = require('../../../src/errors/DetoxRuntimeError');
|
||||
const { filterErrorStack } = require('../../../src/utils/errorUtils');
|
||||
const { DetoxRuntimeError } = require('../../../../src/errors/DetoxRuntimeError');
|
||||
const { filterErrorStack } = require('../../../../src/utils/errorUtils');
|
||||
|
||||
function findUserConstructor() {
|
||||
let wasInBaseClass = false;
|
||||
+2
-2
@@ -3,8 +3,8 @@ const path = require('path');
|
||||
|
||||
const semver = require('semver');
|
||||
|
||||
const detoxPackageJson = require('../../../package.json');
|
||||
const DetoxRuntimeError = require('../../../src/errors/DetoxRuntimeError');
|
||||
const detoxPackageJson = require('../../../../package.json');
|
||||
const { DetoxRuntimeError } = require('../../../../src/errors');
|
||||
|
||||
function assertJestCircus27(maybeProjectConfig) {
|
||||
const projectConfig = maybeProjectConfig.projectConfig || maybeProjectConfig;
|
||||
@@ -1,14 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const lifecycleSymbols = require('../../integration').lifecycle;
|
||||
|
||||
function wrapErrorWithNoopLifecycle(error) {
|
||||
const wrapper = { error };
|
||||
for (const symbol of Object.values(lifecycleSymbols)) {
|
||||
wrapper[symbol] = _.noop;
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
module.exports = wrapErrorWithNoopLifecycle;
|
||||
@@ -16,9 +16,10 @@ function jsify(pathToFile) {
|
||||
.replace(/(?:\/index)?\.js$/, '');
|
||||
}
|
||||
|
||||
const redirectWithWarning = _.memoize((oldPath) => {
|
||||
const newPath = oldPath.replace(SEARCH_VALUE, REPLACE_VALUE);
|
||||
|
||||
const redirectWithWarning = _.memoize((
|
||||
oldPath,
|
||||
newPath = oldPath.replace(SEARCH_VALUE, REPLACE_VALUE)
|
||||
) => {
|
||||
redirectWithWarning._log([
|
||||
'[DEPRECATION] Detox detected an attempt to require a module from an outdated location, please change in your project:',
|
||||
`- ${jsify(oldPath)}`,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = Object.freeze({
|
||||
'userNotificationTriggers': {
|
||||
'push': 'push',
|
||||
'calendar': 'calendar',
|
||||
'timeInterval': 'timeInterval',
|
||||
'location': 'location'
|
||||
},
|
||||
'userActivityTypes': {
|
||||
'searchableItem': 'com.apple.corespotlightitem',
|
||||
'browsingWeb': 'NSUserActivityTypeBrowsingWeb',
|
||||
},
|
||||
'searchableItemActivityIdentifier': 'kCSSearchableItemActivityIdentifier'
|
||||
});
|
||||
@@ -1,140 +0,0 @@
|
||||
// @ts-nocheck
|
||||
const funpermaproxy = require('funpermaproxy');
|
||||
|
||||
const Detox = require('./Detox');
|
||||
const DetoxConstants = require('./DetoxConstants');
|
||||
const configuration = require('./configuration');
|
||||
const logger = require('./utils/logger');
|
||||
const log = logger.child({ __filename });
|
||||
const { trace, traceCall } = require('./utils/trace');
|
||||
|
||||
const _detox = Symbol('detox');
|
||||
const _shouldLogInitError = Symbol('shouldLogInitError');
|
||||
|
||||
class DetoxExportWrapper {
|
||||
constructor() {
|
||||
this[_detox] = Detox.none;
|
||||
this[_shouldLogInitError] = true;
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
this.cleanup = this.cleanup.bind(this);
|
||||
|
||||
this.DetoxConstants = DetoxConstants;
|
||||
|
||||
this._definePassthroughMethod('beforeEach');
|
||||
this._definePassthroughMethod('afterEach');
|
||||
this._definePassthroughMethod('suiteStart');
|
||||
this._definePassthroughMethod('suiteEnd');
|
||||
|
||||
this._definePassthroughMethod('element');
|
||||
this._definePassthroughMethod('expect');
|
||||
this._definePassthroughMethod('waitFor');
|
||||
|
||||
this._defineProxy('by');
|
||||
this._defineProxy('device');
|
||||
this._defineProxy('web');
|
||||
|
||||
this.trace = trace;
|
||||
this.traceCall = traceCall;
|
||||
}
|
||||
|
||||
async init(configOverride, userParams) {
|
||||
let configError, exposeGlobals, resolvedConfig;
|
||||
|
||||
trace.init();
|
||||
logger.reinitialize(Detox.global);
|
||||
|
||||
try {
|
||||
resolvedConfig = await configuration.composeDetoxConfig({
|
||||
override: configOverride,
|
||||
userParams,
|
||||
});
|
||||
|
||||
exposeGlobals = resolvedConfig.behaviorConfig.init.exposeGlobals;
|
||||
} catch (err) {
|
||||
configError = err;
|
||||
exposeGlobals = true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (exposeGlobals) {
|
||||
Detox.none.initContext(Detox.global);
|
||||
}
|
||||
|
||||
if (configError) {
|
||||
throw configError;
|
||||
}
|
||||
|
||||
this[_detox] = new Detox(resolvedConfig);
|
||||
await traceCall('detoxInit', () => this[_detox].init());
|
||||
Detox.none.setError(null);
|
||||
|
||||
return this[_detox];
|
||||
} catch (err) {
|
||||
if (this[_shouldLogInitError]) {
|
||||
log.error({ event: 'DETOX_INIT_ERROR' }, '\n', err);
|
||||
}
|
||||
|
||||
Detox.none.setError(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
Detox.none.cleanupContext(Detox.global);
|
||||
|
||||
if (this[_detox] !== Detox.none) {
|
||||
await this[_detox].cleanup();
|
||||
this[_detox] = Detox.none;
|
||||
}
|
||||
}
|
||||
|
||||
_definePassthroughMethod(name) {
|
||||
this[name] = (...args) => {
|
||||
return this[_detox][name](...args);
|
||||
};
|
||||
}
|
||||
|
||||
_defineProxy(name) {
|
||||
this[name] = funpermaproxy(() => this[_detox][name]);
|
||||
}
|
||||
|
||||
/** Use for test runners with sandboxed global */
|
||||
_setGlobal(global) {
|
||||
Detox.global = global;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_suppressLoggingInitErrors() {
|
||||
this[_shouldLogInitError] = false;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
DetoxExportWrapper.prototype.hook = configuration.hook;
|
||||
|
||||
DetoxExportWrapper.prototype.globalInit = async function() {
|
||||
try {
|
||||
// TODO This can only work in Jest, where config info etc. is available globally through env vars rather
|
||||
// than argv (e.g. in Mocha) -- which we don't have available here.
|
||||
// We will resolve this, ultimately, in https://github.com/wix/Detox/issues/2894 (DAS project), where
|
||||
// this whole hack would be removed altogether.
|
||||
const configs = await configuration.composeDetoxConfig({});
|
||||
await Detox.globalInit(configs);
|
||||
} catch (error) {
|
||||
log.warn({ event: 'GLOBAL_INIT' }, 'An error occurred!');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
DetoxExportWrapper.prototype.globalCleanup = async function() {
|
||||
try {
|
||||
const configs = await configuration.composeDetoxConfig({});
|
||||
await Detox.globalCleanup(configs);
|
||||
} catch (error) {
|
||||
log.warn({ event: 'GLOBAL_CLEANUP' }, 'An error occurred!', error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DetoxExportWrapper;
|
||||
@@ -1,60 +1,20 @@
|
||||
// @ts-nocheck
|
||||
const { URL } = require('url');
|
||||
const util = require('util');
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const lifecycleSymbols = require('../runners/integration').lifecycle;
|
||||
|
||||
const Client = require('./client/Client');
|
||||
const environmentFactory = require('./environmentFactory');
|
||||
const { DetoxRuntimeErrorComposer } = require('./errors');
|
||||
const { InvocationManager } = require('./invoke');
|
||||
const DetoxServer = require('./server/DetoxServer');
|
||||
const symbols = require('./symbols');
|
||||
const AsyncEmitter = require('./utils/AsyncEmitter');
|
||||
const Deferred = require('./utils/Deferred');
|
||||
const MissingDetox = require('./utils/MissingDetox');
|
||||
const logger = require('./utils/logger');
|
||||
const log = logger.child({ __filename });
|
||||
|
||||
const _initHandle = Symbol('_initHandle');
|
||||
const _assertNoPendingInit = Symbol('_assertNoPendingInit');
|
||||
|
||||
class Detox {
|
||||
constructor(config) {
|
||||
log.trace(
|
||||
{ event: 'DETOX_CREATE', config },
|
||||
'created a Detox instance with config:\n%s',
|
||||
util.inspect(_.omit(config, ['errorComposer']), {
|
||||
getters: false,
|
||||
depth: Infinity,
|
||||
maxArrayLength: Infinity,
|
||||
maxStringLength: Infinity,
|
||||
breakLength: false,
|
||||
compact: false,
|
||||
})
|
||||
);
|
||||
|
||||
this[_initHandle] = null;
|
||||
|
||||
for (const [key, symbol] of Object.entries(lifecycleSymbols)) {
|
||||
this[symbol] = (...args) => this._artifactsManager[key](...args);
|
||||
}
|
||||
|
||||
this[lifecycleSymbols.onTestStart] = this.beforeEach;
|
||||
this[lifecycleSymbols.onTestDone] = this.afterEach;
|
||||
|
||||
const { appsConfig, artifactsConfig, behaviorConfig, deviceConfig, sessionConfig } = config;
|
||||
|
||||
this._appsConfig = appsConfig;
|
||||
this._artifactsConfig = artifactsConfig;
|
||||
this._behaviorConfig = behaviorConfig;
|
||||
this._deviceConfig = deviceConfig;
|
||||
this._sessionConfig = sessionConfig;
|
||||
this._runtimeErrorComposer = new DetoxRuntimeErrorComposer({ appsConfig });
|
||||
const uuid = require('./utils/uuid');
|
||||
|
||||
class DetoxWorker {
|
||||
constructor(context) {
|
||||
this._context = context;
|
||||
this._isCleaningUp = false;
|
||||
this._config = context[symbols.config];
|
||||
this._runtimeErrorComposer = new DetoxRuntimeErrorComposer(this._config);
|
||||
this._client = null;
|
||||
this._server = null;
|
||||
this._artifactsManager = null;
|
||||
this._eventEmitter = new AsyncEmitter({
|
||||
events: [
|
||||
@@ -72,24 +32,136 @@ class Detox {
|
||||
onError: this._onEmitError.bind(this),
|
||||
});
|
||||
|
||||
/** @type {string} */
|
||||
this.id = 'worker';
|
||||
/** @type {Detox.Device} */
|
||||
this.device = null;
|
||||
/** @type {Detox.ElementFacade} */
|
||||
this.element = null;
|
||||
/** @type {Detox.WaitForFacade} */
|
||||
this.waitFor = null;
|
||||
/** @type {Detox.ExpectFacade} */
|
||||
this.expect = null;
|
||||
/** @type {Detox.ByFacade} */
|
||||
this.by = null;
|
||||
/** @type {Detox.WebFacade} */
|
||||
this.web = null;
|
||||
|
||||
this._deviceAllocator = null;
|
||||
this._deviceCookie = null;
|
||||
|
||||
this.trace = this._context.trace;
|
||||
/** @deprecated */
|
||||
this.traceCall = this._context.traceCall;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this[_initHandle]) {
|
||||
this[_initHandle] = new Deferred();
|
||||
async init() {
|
||||
if (this._isCleaningUp) return;
|
||||
|
||||
const { resolve, reject } = this[_initHandle];
|
||||
this._doInit().then(resolve, reject);
|
||||
const {
|
||||
apps: appsConfig,
|
||||
artifacts: artifactsConfig,
|
||||
behavior: behaviorConfig,
|
||||
device: deviceConfig,
|
||||
session: sessionConfig
|
||||
} = this._config;
|
||||
|
||||
this._appsConfig = appsConfig;
|
||||
this._artifactsConfig = artifactsConfig;
|
||||
this._behaviorConfig = behaviorConfig;
|
||||
this._deviceConfig = deviceConfig;
|
||||
this._sessionConfig = sessionConfig;
|
||||
this._sessionConfig.sessionId = sessionConfig.sessionId || uuid.UUID();
|
||||
this._runtimeErrorComposer.appsConfig = this._appsConfig;
|
||||
|
||||
this._client = new Client(sessionConfig);
|
||||
this._client.terminateApp = async () => {
|
||||
// @ts-ignore
|
||||
if (this.device && this.device._isAppRunning()) {
|
||||
await this.device.terminateApp();
|
||||
}
|
||||
};
|
||||
|
||||
await this._client.connect();
|
||||
if (this._isCleaningUp) return;
|
||||
|
||||
const invocationManager = new InvocationManager(this._client);
|
||||
|
||||
const {
|
||||
// @ts-ignore
|
||||
envValidatorFactory,
|
||||
deviceAllocatorFactory,
|
||||
// @ts-ignore
|
||||
artifactsManagerFactory,
|
||||
// @ts-ignore
|
||||
matchersFactory,
|
||||
// @ts-ignore
|
||||
runtimeDeviceFactory,
|
||||
} = environmentFactory.createFactories(this._deviceConfig);
|
||||
|
||||
const envValidator = envValidatorFactory.createValidator();
|
||||
await envValidator.validate();
|
||||
if (this._isCleaningUp) return;
|
||||
|
||||
const commonDeps = {
|
||||
invocationManager,
|
||||
client: this._client,
|
||||
eventEmitter: this._eventEmitter,
|
||||
runtimeErrorComposer: this._runtimeErrorComposer,
|
||||
};
|
||||
|
||||
this._artifactsManager = artifactsManagerFactory.createArtifactsManager(this._artifactsConfig, commonDeps);
|
||||
this._deviceAllocator = deviceAllocatorFactory.createDeviceAllocator(commonDeps);
|
||||
this._deviceCookie = await this._deviceAllocator.allocate(this._deviceConfig);
|
||||
if (this._isCleaningUp) return;
|
||||
|
||||
this.device = runtimeDeviceFactory.createRuntimeDevice(
|
||||
this._deviceCookie,
|
||||
commonDeps,
|
||||
{
|
||||
appsConfig: this._appsConfig,
|
||||
behaviorConfig: this._behaviorConfig,
|
||||
deviceConfig: this._deviceConfig,
|
||||
sessionConfig,
|
||||
});
|
||||
if (this._isCleaningUp) return;
|
||||
|
||||
const matchers = matchersFactory.createMatchers({
|
||||
invocationManager,
|
||||
runtimeDevice: this.device,
|
||||
eventEmitter: this._eventEmitter,
|
||||
});
|
||||
Object.assign(this, matchers);
|
||||
|
||||
if (behaviorConfig.init.exposeGlobals) {
|
||||
Object.assign(DetoxWorker.global, {
|
||||
...matchers,
|
||||
device: this.device,
|
||||
detox: this,
|
||||
});
|
||||
}
|
||||
|
||||
return this[_initHandle].promise;
|
||||
// @ts-ignore
|
||||
await this.device.installUtilBinaries();
|
||||
if (this._isCleaningUp) return;
|
||||
|
||||
if (behaviorConfig.init.reinstallApp) {
|
||||
await this._reinstallAppsOnDevice();
|
||||
if (this._isCleaningUp) return;
|
||||
}
|
||||
|
||||
const appAliases = Object.keys(this._appsConfig);
|
||||
if (appAliases.length === 1) {
|
||||
await this.device.selectApp(appAliases[0]);
|
||||
} else {
|
||||
await this.device.selectApp(null);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
await this[_assertNoPendingInit]().catch(_.noop);
|
||||
this._isCleaningUp = true;
|
||||
|
||||
if (this._artifactsManager) {
|
||||
await this._artifactsManager.onBeforeCleanup();
|
||||
@@ -103,143 +175,59 @@ class Detox {
|
||||
}
|
||||
|
||||
if (this.device) {
|
||||
const shutdown = this._behaviorConfig.cleanup.shutdownDevice;
|
||||
const shutdown = this._behaviorConfig ? this._behaviorConfig.cleanup.shutdownDevice : false;
|
||||
// @ts-ignore
|
||||
await this.device._cleanup();
|
||||
await this._deviceAllocator.free(this._deviceCookie, { shutdown });
|
||||
}
|
||||
|
||||
if (this._server) {
|
||||
await this._server.close();
|
||||
this._server = null;
|
||||
}
|
||||
|
||||
this._deviceAllocator = null;
|
||||
this._deviceCookie = null;
|
||||
this.device = null;
|
||||
}
|
||||
|
||||
async beforeEach(testSummary) {
|
||||
await this[_assertNoPendingInit]();
|
||||
get log() {
|
||||
return this._context.log;
|
||||
}
|
||||
|
||||
onRunStart = async (...args) => this._artifactsManager.onRunStart(...args);
|
||||
onRunDescribeStart = async (...args) => this._artifactsManager.onRunDescribeStart(...args);
|
||||
onTestStart = async (testSummary) => {
|
||||
if (this._isCleaningUp) return;
|
||||
this._validateTestSummary('beforeEach', testSummary);
|
||||
this._logTestRunCheckpoint('DETOX_BEFORE_EACH', testSummary);
|
||||
|
||||
if (this._isCleaningUp) return;
|
||||
await this._dumpUnhandledErrorsIfAny({
|
||||
pendingRequests: false,
|
||||
testName: testSummary.fullName,
|
||||
});
|
||||
|
||||
if (this._isCleaningUp) return;
|
||||
await this._artifactsManager.onTestStart(testSummary);
|
||||
}
|
||||
|
||||
async afterEach(testSummary) {
|
||||
await this[_assertNoPendingInit]();
|
||||
|
||||
};
|
||||
onHookStart = async (...args) => this._artifactsManager.onHookStart(...args);
|
||||
onHookFailure = async (...args) => this._artifactsManager.onHookFailure(...args);
|
||||
onHookSuccess = async (...args) => this._artifactsManager.onHookSuccess(...args);
|
||||
onTestFnStart = async (...args) => this._artifactsManager.onTestFnStart(...args);
|
||||
onTestFnFailure = async (...args) => this._artifactsManager.onTestFnFailure(...args);
|
||||
onTestFnSuccess = async (...args) => this._artifactsManager.onTestFnSuccess(...args);
|
||||
onTestDone = async (testSummary) => {
|
||||
if (this._isCleaningUp) return;
|
||||
this._validateTestSummary('afterEach', testSummary);
|
||||
this._logTestRunCheckpoint('DETOX_AFTER_EACH', testSummary);
|
||||
|
||||
if (this._isCleaningUp) return;
|
||||
await this._artifactsManager.onTestDone(testSummary);
|
||||
|
||||
if (this._isCleaningUp) return;
|
||||
await this._dumpUnhandledErrorsIfAny({
|
||||
pendingRequests: testSummary.timedOut,
|
||||
testName: testSummary.fullName,
|
||||
});
|
||||
}
|
||||
|
||||
async _doInit() {
|
||||
const behaviorConfig = this._behaviorConfig.init;
|
||||
const sessionConfig = this._sessionConfig;
|
||||
|
||||
if (sessionConfig.autoStart) {
|
||||
this._server = new DetoxServer({
|
||||
port: sessionConfig.server
|
||||
? new URL(sessionConfig.server).port
|
||||
: 0,
|
||||
standalone: false,
|
||||
});
|
||||
|
||||
await this._server.open();
|
||||
|
||||
if (!sessionConfig.server) {
|
||||
sessionConfig.server = `ws://localhost:${this._server.port}`;
|
||||
}
|
||||
}
|
||||
|
||||
this._client = new Client(sessionConfig);
|
||||
this._client.terminateApp = async () => {
|
||||
if (this.device && this.device._isAppRunning()) {
|
||||
await this.device.terminateApp();
|
||||
}
|
||||
};
|
||||
|
||||
await this._client.connect();
|
||||
|
||||
const invocationManager = new InvocationManager(this._client);
|
||||
|
||||
const {
|
||||
envValidatorFactory,
|
||||
deviceAllocatorFactory,
|
||||
artifactsManagerFactory,
|
||||
matchersFactory,
|
||||
runtimeDeviceFactory,
|
||||
} = environmentFactory.createFactories(this._deviceConfig);
|
||||
|
||||
const envValidator = envValidatorFactory.createValidator();
|
||||
await envValidator.validate();
|
||||
|
||||
const commonDeps = {
|
||||
invocationManager,
|
||||
client: this._client,
|
||||
eventEmitter: this._eventEmitter,
|
||||
runtimeErrorComposer: this._runtimeErrorComposer,
|
||||
};
|
||||
|
||||
this._artifactsManager = artifactsManagerFactory.createArtifactsManager(this._artifactsConfig, commonDeps);
|
||||
|
||||
this._deviceAllocator = deviceAllocatorFactory.createDeviceAllocator(commonDeps);
|
||||
this._deviceCookie = await this._deviceAllocator.allocate(this._deviceConfig);
|
||||
|
||||
this.device = runtimeDeviceFactory.createRuntimeDevice(
|
||||
this._deviceCookie,
|
||||
commonDeps,
|
||||
{
|
||||
appsConfig: this._appsConfig,
|
||||
behaviorConfig: this._behaviorConfig,
|
||||
deviceConfig: this._deviceConfig,
|
||||
sessionConfig,
|
||||
});
|
||||
await this.device._prepare();
|
||||
|
||||
const matchers = matchersFactory.createMatchers({
|
||||
invocationManager,
|
||||
runtimeDevice: this.device,
|
||||
eventEmitter: this._eventEmitter,
|
||||
});
|
||||
Object.assign(this, matchers);
|
||||
|
||||
if (behaviorConfig.exposeGlobals) {
|
||||
Object.assign(Detox.global, {
|
||||
...matchers,
|
||||
device: this.device,
|
||||
});
|
||||
}
|
||||
|
||||
await this.device.installUtilBinaries();
|
||||
if (behaviorConfig.reinstallApp) {
|
||||
await this._reinstallAppsOnDevice();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
[_assertNoPendingInit]() {
|
||||
const handle = this[_initHandle];
|
||||
if (!handle) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (handle.status === Deferred.PENDING) {
|
||||
handle.reject(this._runtimeErrorComposer.abortedDetoxInit());
|
||||
}
|
||||
|
||||
return handle.promise;
|
||||
}
|
||||
};
|
||||
onRunDescribeFinish = async (...args) => this._artifactsManager.onRunDescribeFinish(...args);
|
||||
onRunFinish = async (...args) => this._artifactsManager.onRunFinish(...args);
|
||||
|
||||
async _reinstallAppsOnDevice() {
|
||||
const appNames = _(this._appsConfig)
|
||||
@@ -250,21 +238,21 @@ class Detox {
|
||||
|
||||
for (const appName of appNames) {
|
||||
await this.device.selectApp(appName);
|
||||
if (this._isCleaningUp) return;
|
||||
await this.device.uninstallApp();
|
||||
if (this._isCleaningUp) return;
|
||||
}
|
||||
|
||||
for (const appName of appNames) {
|
||||
await this.device.selectApp(appName);
|
||||
if (this._isCleaningUp) return;
|
||||
await this.device.installApp();
|
||||
}
|
||||
|
||||
if (appNames.length !== 1) {
|
||||
await this.device.selectApp(null);
|
||||
if (this._isCleaningUp) return;
|
||||
}
|
||||
}
|
||||
|
||||
_logTestRunCheckpoint(event, { status, fullName }) {
|
||||
log.trace({ event, status }, `${status} test: ${JSON.stringify(fullName)}`);
|
||||
this.log.trace({ event, status }, `${status} test: ${JSON.stringify(fullName)}`);
|
||||
}
|
||||
|
||||
_validateTestSummary(methodName, testSummary) {
|
||||
@@ -289,29 +277,17 @@ class Detox {
|
||||
}
|
||||
|
||||
_onEmitError({ error, eventName, eventObj }) {
|
||||
log.error(
|
||||
this.log.error(
|
||||
{ event: 'EMIT_ERROR', fn: eventName },
|
||||
`Caught an exception in: emitter.emit("${eventName}", ${JSON.stringify(eventObj)})\n\n`,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
static async globalInit(configs) {
|
||||
const handler = await environmentFactory.createGlobalLifecycleHandler(configs.deviceConfig);
|
||||
if (handler) {
|
||||
await handler.globalInit();
|
||||
}
|
||||
}
|
||||
|
||||
static async globalCleanup(configs) {
|
||||
const handler = await environmentFactory.createGlobalLifecycleHandler(configs.deviceConfig);
|
||||
if (handler) {
|
||||
await handler.globalCleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Detox.none = new MissingDetox();
|
||||
Detox.global = global;
|
||||
/**
|
||||
* @type {NodeJS.Global}
|
||||
*/
|
||||
DetoxWorker.global = global;
|
||||
|
||||
module.exports = Detox;
|
||||
module.exports = DetoxWorker;
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
const testSummaries = require('./artifacts/__mocks__/testSummaries.mock');
|
||||
const configuration = require('./configuration');
|
||||
const Deferred = require('./utils/Deferred');
|
||||
|
||||
jest.mock('./utils/logger');
|
||||
jest.mock('./client/Client');
|
||||
@@ -9,16 +10,7 @@ jest.mock('./invoke');
|
||||
jest.mock('./utils/wrapWithStackTraceCutter');
|
||||
jest.mock('./environmentFactory');
|
||||
|
||||
jest.mock('./server/DetoxServer', () => {
|
||||
const FakeServer = jest.genMockFromModule('./server/DetoxServer');
|
||||
return jest.fn().mockImplementation(() => {
|
||||
const server = new FakeServer();
|
||||
server.port = 12345;
|
||||
return server;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Detox', () => {
|
||||
describe('DetoxWorker', () => {
|
||||
const fakeCookie = {
|
||||
chocolate: 'yum',
|
||||
};
|
||||
@@ -28,17 +20,8 @@ describe('Detox', () => {
|
||||
const eventEmitter = () => AsyncEmitter.mock.instances[0];
|
||||
eventEmitter.errorCallback = () => AsyncEmitter.mock.calls[0][0].onError;
|
||||
|
||||
const suspendMethod = (obj, methodName) => {
|
||||
let releaseFn;
|
||||
const promise = new Promise((resolve) => { releaseFn = resolve; });
|
||||
obj[methodName].mockReturnValue(promise);
|
||||
return releaseFn;
|
||||
};
|
||||
const suspendAllocation = () => suspendMethod(deviceAllocator, 'allocate');
|
||||
const suspendAppUninstall = () => suspendMethod(runtimeDevice, 'uninstallApp');
|
||||
const suspendAppInstall = () => suspendMethod(runtimeDevice, 'installApp');
|
||||
|
||||
let detoxConfig;
|
||||
let detoxContext;
|
||||
|
||||
let envValidatorFactory;
|
||||
let artifactsManagerFactory;
|
||||
@@ -50,14 +33,12 @@ describe('Detox', () => {
|
||||
let logger;
|
||||
let Client;
|
||||
let AsyncEmitter;
|
||||
let DetoxServer;
|
||||
let invoke;
|
||||
let envValidator;
|
||||
let deviceAllocator;
|
||||
let runtimeDevice;
|
||||
let Detox;
|
||||
let detox;
|
||||
let lifecycleSymbols;
|
||||
|
||||
beforeEach(() => {
|
||||
mockEnvironmentFactories();
|
||||
@@ -94,17 +75,17 @@ describe('Detox', () => {
|
||||
invoke = require('./invoke');
|
||||
Client = require('./client/Client');
|
||||
AsyncEmitter = require('./utils/AsyncEmitter');
|
||||
DetoxServer = require('./server/DetoxServer');
|
||||
lifecycleSymbols = require('../runners/integration').lifecycle;
|
||||
|
||||
Detox = require('./Detox');
|
||||
Detox = require('./DetoxWorker');
|
||||
const symbols = require('./symbols');
|
||||
detoxContext = { log: logger, [symbols.config]: detoxConfig };
|
||||
});
|
||||
|
||||
describe('when detox.init() is called', () => {
|
||||
describe('when DetoxWorker#init() is called', () => {
|
||||
let mockGlobalMatcher;
|
||||
|
||||
const init = async () => {
|
||||
detox = await new Detox(detoxConfig).init();
|
||||
detox = await new Detox(detoxContext).init();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -123,17 +104,8 @@ describe('Detox', () => {
|
||||
describe('', () => {
|
||||
beforeEach(init);
|
||||
|
||||
it('should create a DetoxServer automatically', () =>
|
||||
expect(DetoxServer).toHaveBeenCalledWith({
|
||||
port: 0,
|
||||
standalone: false,
|
||||
}));
|
||||
|
||||
it('should create a new Client', () =>
|
||||
expect(Client).toHaveBeenCalledWith(expect.objectContaining({
|
||||
server: 'ws://localhost:12345',
|
||||
sessionId: expect.any(String),
|
||||
})));
|
||||
expect(Client).toHaveBeenCalled());
|
||||
|
||||
it('should create an invocation manager', () =>
|
||||
expect(invoke.InvocationManager).toHaveBeenCalledWith(client()));
|
||||
@@ -155,7 +127,7 @@ describe('Detox', () => {
|
||||
expect(envValidator.validate).toHaveBeenCalled());
|
||||
|
||||
it('should allocate a device', () => {
|
||||
expect(deviceAllocator.allocate).toHaveBeenCalledWith(detoxConfig.deviceConfig);
|
||||
expect(deviceAllocator.allocate).toHaveBeenCalledWith(detoxConfig.device);
|
||||
});
|
||||
|
||||
it('should create a runtime-device based on the allocation result (cookie)', () =>
|
||||
@@ -168,9 +140,9 @@ describe('Detox', () => {
|
||||
runtimeErrorComposer: expect.any(Object),
|
||||
},
|
||||
{
|
||||
appsConfig: detoxConfig.appsConfig,
|
||||
behaviorConfig: detoxConfig.behaviorConfig,
|
||||
deviceConfig: detoxConfig.deviceConfig,
|
||||
appsConfig: detoxConfig.apps,
|
||||
behaviorConfig: detoxConfig.behavior,
|
||||
deviceConfig: detoxConfig.device,
|
||||
sessionConfig: expect.any(Object),
|
||||
},
|
||||
));
|
||||
@@ -196,14 +168,11 @@ describe('Detox', () => {
|
||||
expect(global.globalMatcher).toBe(mockGlobalMatcher));
|
||||
|
||||
it('should create artifacts manager', () =>
|
||||
expect(artifactsManagerFactory.createArtifactsManager).toHaveBeenCalledWith(detoxConfig.artifactsConfig, expect.objectContaining({
|
||||
expect(artifactsManagerFactory.createArtifactsManager).toHaveBeenCalledWith(detoxConfig.artifacts, expect.objectContaining({
|
||||
client: client(),
|
||||
eventEmitter: eventEmitter(),
|
||||
})));
|
||||
|
||||
it('should prepare the device', () =>
|
||||
expect(runtimeDevice._prepare).toHaveBeenCalled());
|
||||
|
||||
it('should select and reinstall the app', () => {
|
||||
expect(runtimeDevice.selectApp).toHaveBeenCalledWith('default');
|
||||
expect(runtimeDevice.uninstallApp).toHaveBeenCalled();
|
||||
@@ -218,23 +187,14 @@ describe('Detox', () => {
|
||||
expect(await detox.init()).toBeInstanceOf(Detox));
|
||||
});
|
||||
|
||||
it('should return the same promise on consequent calls', () => {
|
||||
detox = new Detox(detoxConfig);
|
||||
|
||||
const initPromise1 = detox.init();
|
||||
const initPromise2 = detox.init();
|
||||
|
||||
expect(initPromise1).toBe(initPromise2);
|
||||
});
|
||||
|
||||
describe('with multiple apps', () => {
|
||||
beforeEach(() => {
|
||||
detoxConfig.appsConfig['extraApp'] = {
|
||||
detoxConfig.apps['extraApp'] = {
|
||||
type: 'ios.app',
|
||||
binaryPath: 'path/to/app',
|
||||
};
|
||||
|
||||
detoxConfig.appsConfig['extraAppWithAnotherArguments'] = {
|
||||
detoxConfig.apps['extraAppWithAnotherArguments'] = {
|
||||
type: 'ios.app',
|
||||
binaryPath: 'path/to/app',
|
||||
launchArgs: {
|
||||
@@ -258,28 +218,9 @@ describe('Detox', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with sessionConfig.autoStart undefined', () => {
|
||||
beforeEach(() => { delete detoxConfig.sessionConfig.autoStart; });
|
||||
beforeEach(init);
|
||||
|
||||
it('should not start DetoxServer', () =>
|
||||
expect(DetoxServer).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
describe('with sessionConfig.server custom URL', () => {
|
||||
beforeEach(() => { detoxConfig.sessionConfig.server = 'ws://localhost:451'; });
|
||||
beforeEach(init);
|
||||
|
||||
it('should create a DetoxServer using the port from that URL', () =>
|
||||
expect(DetoxServer).toHaveBeenCalledWith({
|
||||
port: '451',
|
||||
standalone: false,
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with behaviorConfig.init.exposeGlobals = false', () => {
|
||||
describe('with behavior.init.exposeGlobals = false', () => {
|
||||
beforeEach(() => {
|
||||
detoxConfig.behaviorConfig.init.exposeGlobals = false;
|
||||
detoxConfig.behavior.init.exposeGlobals = false;
|
||||
});
|
||||
|
||||
beforeEach(init);
|
||||
@@ -298,16 +239,13 @@ describe('Detox', () => {
|
||||
expect(global.globalMatcher).toBe(undefined));
|
||||
});
|
||||
|
||||
describe('with behaviorConfig.init.reinstallApp = false', () => {
|
||||
describe('with behavior.init.reinstallApp = false', () => {
|
||||
beforeEach(() => {
|
||||
detoxConfig.behaviorConfig.init.reinstallApp = false;
|
||||
detoxConfig.behavior.init.reinstallApp = false;
|
||||
});
|
||||
|
||||
beforeEach(init);
|
||||
|
||||
it('should prepare the device', () =>
|
||||
expect(runtimeDevice._prepare).toHaveBeenCalled());
|
||||
|
||||
it('should not reinstall the app', () => {
|
||||
expect(runtimeDevice.uninstallApp).not.toHaveBeenCalled();
|
||||
expect(runtimeDevice.installApp).not.toHaveBeenCalled();
|
||||
@@ -346,203 +284,210 @@ describe('Detox', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when detox.beforeEach() is called', () => {
|
||||
describe('before detox.init() is called', () => {
|
||||
beforeEach(() => {
|
||||
detox = new Detox(detoxConfig);
|
||||
});
|
||||
|
||||
it('should throw an error', () =>
|
||||
expect(detox.beforeEach(testSummaries.running())).rejects.toThrowError(/of null/));
|
||||
describe('when DetoxWorker#@onTestStart() is called', () => {
|
||||
beforeEach(async () => {
|
||||
detox = await new Detox(detoxContext).init();
|
||||
});
|
||||
|
||||
describe('before detox.init() is finished', () => {
|
||||
beforeEach(() => {
|
||||
detox = new Detox(detoxConfig);
|
||||
});
|
||||
|
||||
it('should throw an error', async () => {
|
||||
const initPromise = detox.init();
|
||||
await expect(detox.beforeEach(testSummaries.running())).rejects.toThrowError(/Aborted detox.init/);
|
||||
await expect(initPromise).rejects.toThrowError(/Aborted detox.init/);
|
||||
});
|
||||
it('should validate test summary object', async () => {
|
||||
await expect(detox.onTestStart('Test')).rejects.toThrowError(
|
||||
/Invalid test summary was passed/
|
||||
);
|
||||
});
|
||||
|
||||
describe('after detox.init() is finished', () => {
|
||||
beforeEach(async () => {
|
||||
detox = await new Detox(detoxConfig).init();
|
||||
it('should validate test summary status', async () => {
|
||||
await expect(detox.onTestStart({
|
||||
...testSummaries.running(),
|
||||
status: undefined,
|
||||
})).rejects.toThrowError(/Invalid test summary status/);
|
||||
});
|
||||
|
||||
it('should validate test summary status', async () => {
|
||||
await expect(detox.onTestStart({
|
||||
...testSummaries.running(),
|
||||
status: undefined,
|
||||
})).rejects.toThrowError(/Invalid test summary status/);
|
||||
});
|
||||
|
||||
describe('with a valid test summary', () => {
|
||||
beforeEach(() => detox.onTestStart(testSummaries.running()));
|
||||
|
||||
it('should trace DETOX_BEFORE_EACH event', () =>
|
||||
expect(logger.trace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ event: 'DETOX_BEFORE_EACH' }),
|
||||
expect.any(String)
|
||||
));
|
||||
|
||||
it('should notify artifacts manager about "testStart', () =>
|
||||
expect(artifactsManager.onTestStart).toHaveBeenCalledWith(testSummaries.running()));
|
||||
|
||||
it('should not relaunch app', async () => {
|
||||
await detox.onTestStart(testSummaries.running());
|
||||
expect(runtimeDevice.launchApp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate test summary object', async () => {
|
||||
await expect(detox.beforeEach('Test')).rejects.toThrowError(
|
||||
/Invalid test summary was passed/
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate test summary status', async () => {
|
||||
await expect(detox.beforeEach({
|
||||
...testSummaries.running(),
|
||||
status: undefined,
|
||||
})).rejects.toThrowError(/Invalid test summary status/);
|
||||
});
|
||||
|
||||
it('should validate test summary status', async () => {
|
||||
await expect(detox.beforeEach({
|
||||
...testSummaries.running(),
|
||||
status: undefined,
|
||||
})).rejects.toThrowError(/Invalid test summary status/);
|
||||
});
|
||||
|
||||
describe('with a valid test summary', () => {
|
||||
beforeEach(() => detox.beforeEach(testSummaries.running()));
|
||||
|
||||
it('should trace DETOX_BEFORE_EACH event', () =>
|
||||
expect(logger.trace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ event: 'DETOX_BEFORE_EACH' }),
|
||||
expect.any(String)
|
||||
));
|
||||
|
||||
it('should notify artifacts manager about "testStart', () =>
|
||||
expect(artifactsManager.onTestStart).toHaveBeenCalledWith(testSummaries.running()));
|
||||
|
||||
it('should not relaunch app', async () => {
|
||||
await detox.beforeEach(testSummaries.running());
|
||||
expect(runtimeDevice.launchApp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not dump pending network requests', async () => {
|
||||
await detox.beforeEach(testSummaries.running());
|
||||
expect(client().dumpPendingRequests).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should not dump pending network requests', async () => {
|
||||
await detox.onTestStart(testSummaries.running());
|
||||
expect(client().dumpPendingRequests).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when detox.afterEach() is called', () => {
|
||||
describe('before detox.init() is called', () => {
|
||||
beforeEach(() => {
|
||||
detox = new Detox(detoxConfig);
|
||||
});
|
||||
|
||||
it('should throw an error', () =>
|
||||
expect(detox.afterEach(testSummaries.running())).rejects.toThrowError(/of null/));
|
||||
describe('when DetoxWorker#@onTestDone() is called', () => {
|
||||
beforeEach(async () => {
|
||||
detox = await new Detox(detoxContext).init();
|
||||
await detox.onTestStart(testSummaries.running());
|
||||
});
|
||||
|
||||
describe('before detox.init() is finished', () => {
|
||||
beforeEach(() => {
|
||||
detox = new Detox(detoxConfig);
|
||||
});
|
||||
it('should validate non-object test summary', () =>
|
||||
expect(detox.onTestDone).rejects.toThrowError(/Invalid test summary was passed/));
|
||||
|
||||
it('should throw an error', async () => {
|
||||
const initPromise = detox.init();
|
||||
await expect(detox.afterEach(testSummaries.running())).rejects.toThrowError(/Aborted detox.init/);
|
||||
await expect(initPromise).rejects.toThrowError(/Aborted detox.init/);
|
||||
});
|
||||
it('should validate against invalid test summary status', () =>
|
||||
expect(detox.onTestDone({})).rejects.toThrowError(/Invalid test summary status/));
|
||||
|
||||
describe('with a passing test summary', () => {
|
||||
beforeEach(() => detox.onTestDone(testSummaries.passed()));
|
||||
|
||||
it('should trace DETOX_AFTER_EACH event', () =>
|
||||
expect(logger.trace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ event: 'DETOX_AFTER_EACH' }),
|
||||
expect.any(String)
|
||||
));
|
||||
|
||||
it('should notify artifacts manager about "testDone"', () =>
|
||||
expect(artifactsManager.onTestDone).toHaveBeenCalledWith(testSummaries.passed()));
|
||||
});
|
||||
|
||||
describe('after detox.init() is finished', () => {
|
||||
beforeEach(async () => {
|
||||
detox = await new Detox(detoxConfig).init();
|
||||
await detox.beforeEach(testSummaries.running());
|
||||
});
|
||||
describe('with a failed test summary (due to failed asseration)', () => {
|
||||
beforeEach(() => detox.onTestDone(testSummaries.failed()));
|
||||
|
||||
it('should validate non-object test summary', () =>
|
||||
expect(detox.afterEach()).rejects.toThrowError(/Invalid test summary was passed/));
|
||||
it('should not dump pending network requests', () =>
|
||||
expect(client().dumpPendingRequests).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should validate against invalid test summary status', () =>
|
||||
expect(detox.afterEach({})).rejects.toThrowError(/Invalid test summary status/));
|
||||
describe('with a failed test summary (due to a timeout)', () => {
|
||||
beforeEach(() => detox.onTestDone(testSummaries.timedOut()));
|
||||
|
||||
describe('with a passing test summary', () => {
|
||||
beforeEach(() => detox.afterEach(testSummaries.passed()));
|
||||
|
||||
it('should trace DETOX_AFTER_EACH event', () =>
|
||||
expect(logger.trace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ event: 'DETOX_AFTER_EACH' }),
|
||||
expect.any(String)
|
||||
));
|
||||
|
||||
it('should notify artifacts manager about "testDone"', () =>
|
||||
expect(artifactsManager.onTestDone).toHaveBeenCalledWith(testSummaries.passed()));
|
||||
});
|
||||
|
||||
describe('with a failed test summary (due to failed asseration)', () => {
|
||||
beforeEach(() => detox.afterEach(testSummaries.failed()));
|
||||
|
||||
it('should not dump pending network requests', () =>
|
||||
expect(client().dumpPendingRequests).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
describe('with a failed test summary (due to a timeout)', () => {
|
||||
beforeEach(() => detox.afterEach(testSummaries.timedOut()));
|
||||
|
||||
it('should dump pending network requests', () =>
|
||||
expect(client().dumpPendingRequests).toHaveBeenCalled());
|
||||
});
|
||||
it('should dump pending network requests', () =>
|
||||
expect(client().dumpPendingRequests).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
describe('when detox.cleanup() is called', () => {
|
||||
describe('when DetoxWorker#cleanup() is called', () => {
|
||||
let deferred;
|
||||
let initPromise;
|
||||
|
||||
const startInit = () => {
|
||||
detox = new Detox(detoxConfig);
|
||||
detox = new Detox(detoxContext);
|
||||
initPromise = detox.init();
|
||||
};
|
||||
|
||||
describe('before detox.init() has been called', () => {
|
||||
it(`should not throw`, async () =>
|
||||
await expect(new Detox(detoxConfig).cleanup()).resolves.not.toThrowError());
|
||||
beforeEach(() => {
|
||||
deferred = new Deferred();
|
||||
});
|
||||
|
||||
describe('before client has connected', () => {
|
||||
beforeEach(() => Client.setInfiniteConnect());
|
||||
beforeEach(startInit);
|
||||
describe('before detox.init()', () => {
|
||||
describe('has been called', () => {
|
||||
it(`should not throw`, async () => {
|
||||
await expect(new Detox(detoxContext).cleanup()).resolves.not.toThrowError();
|
||||
});
|
||||
|
||||
it(`should not throw, but should reject detox.init() promise`, async () => {
|
||||
await expect(detox.cleanup()).resolves.not.toThrowError();
|
||||
await expect(initPromise).rejects.toThrowError(/Aborted detox.init.*execution/);
|
||||
it(`should not try to create a Websocket client`, async () => {
|
||||
detox = new Detox(detoxContext);
|
||||
await expect(Promise.all([detox.cleanup(), detox.init()])).resolves.not.toThrowError();
|
||||
await expect(Client).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('before device has been allocated', () => {
|
||||
let releaseFn;
|
||||
beforeEach(() => { releaseFn = suspendAllocation(); });
|
||||
beforeEach(startInit);
|
||||
afterEach(() => releaseFn());
|
||||
describe('connects WS client', () => {
|
||||
beforeEach(() => Client.setInfiniteConnect());
|
||||
beforeEach(startInit);
|
||||
|
||||
it(`should not throw, but should reject detox.init() promise`, async () => {
|
||||
await expect(detox.cleanup()).resolves.not.toThrowError();
|
||||
await expect(initPromise).rejects.toThrowError(/Aborted detox.init.*execution/);
|
||||
it(`should stop the execution and skip creation of the invocation manager`, async () => {
|
||||
await expect(detox.cleanup()).resolves.not.toThrow();
|
||||
await expect(initPromise).rejects.toThrowError(/Fake error/);
|
||||
await expect(invocationManager()).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('before app has been uninstalled', () => {
|
||||
let releaseFn;
|
||||
beforeEach(() => { releaseFn = suspendAppUninstall(); });
|
||||
beforeEach(startInit);
|
||||
afterEach(() => releaseFn());
|
||||
describe('validates the config', () => {
|
||||
beforeEach(() => {
|
||||
envValidator.validate.mockReturnValue(deferred.promise);
|
||||
});
|
||||
|
||||
it(`should not throw, but should reject detox.init() promise`, async () => {
|
||||
await expect(detox.cleanup()).resolves.not.toThrowError();
|
||||
await expect(initPromise).rejects.toThrowError(/Aborted detox.init.*execution/);
|
||||
beforeEach(startInit);
|
||||
|
||||
it(`should stop the execution and skip allocating the device`, async () => {
|
||||
await expect(detox.cleanup()).resolves.not.toThrowError();
|
||||
deferred.resolve();
|
||||
await expect(initPromise).resolves.toBe(undefined);
|
||||
await expect(deviceAllocator.allocate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('before app has been installed', () => {
|
||||
let releaseFn;
|
||||
beforeEach(() => { releaseFn = suspendAppInstall(); });
|
||||
beforeEach(startInit);
|
||||
afterEach(() => releaseFn());
|
||||
describe('allocates the device', () => {
|
||||
beforeEach(() => {
|
||||
deviceAllocator.allocate.mockReturnValue(deferred.promise);
|
||||
});
|
||||
|
||||
it(`should not throw, but should reject detox.init() promise`, async () => {
|
||||
await expect(detox.cleanup()).resolves.not.toThrowError();
|
||||
await expect(initPromise).rejects.toThrowError(/Aborted detox.init.*execution/);
|
||||
beforeEach(startInit);
|
||||
|
||||
it(`should stop the execution and skip uninstall the app`, async () => {
|
||||
await expect(detox.cleanup()).resolves.not.toThrowError();
|
||||
deferred.resolve();
|
||||
await expect(initPromise).resolves.toBe(undefined);
|
||||
await expect(runtimeDevice.installUtilBinaries).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('before util binaries have been installed', () => {
|
||||
beforeEach(() => {
|
||||
runtimeDevice.installUtilBinaries.mockReturnValue(deferred.promise);
|
||||
});
|
||||
|
||||
beforeEach(startInit);
|
||||
|
||||
it(`should stop the execution and skip uninstalling the app`, async () => {
|
||||
await expect(detox.cleanup()).resolves.not.toThrowError();
|
||||
deferred.resolve();
|
||||
await expect(initPromise).resolves.toBe(undefined);
|
||||
await expect(runtimeDevice.uninstallApp).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('before app has been uninstalled', () => {
|
||||
beforeEach(() => {
|
||||
runtimeDevice.uninstallApp.mockReturnValue(deferred.promise);
|
||||
});
|
||||
|
||||
beforeEach(startInit);
|
||||
|
||||
it(`should stop the execution and skip installing the app`, async () => {
|
||||
await expect(detox.cleanup()).resolves.not.toThrowError();
|
||||
deferred.resolve();
|
||||
await expect(initPromise).resolves.not.toThrowError();
|
||||
await expect(runtimeDevice.installApp).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('before app has been installed', () => {
|
||||
beforeEach(() => {
|
||||
runtimeDevice.installApp.mockReturnValue(deferred.promise);
|
||||
});
|
||||
|
||||
beforeEach(startInit);
|
||||
|
||||
it(`should not throw, but should reject detox.init() promise`, async () => {
|
||||
await expect(detox.cleanup()).resolves.not.toThrowError();
|
||||
deferred.resolve();
|
||||
await expect(initPromise).resolves.not.toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('after detox.init()', () => {
|
||||
beforeEach(async () => {
|
||||
detox = new Detox(detoxConfig);
|
||||
detox = new Detox(detoxContext);
|
||||
await detox.init();
|
||||
});
|
||||
|
||||
@@ -576,10 +521,10 @@ describe('Detox', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when behaviorConfig.cleanup.shutdownDevice = true', () => {
|
||||
describe('when behavior.cleanup.shutdownDevice = true', () => {
|
||||
beforeEach(async () => {
|
||||
detoxConfig.behaviorConfig.cleanup.shutdownDevice = true;
|
||||
detox = await new Detox(detoxConfig).init();
|
||||
detoxConfig.behavior.cleanup.shutdownDevice = true;
|
||||
detox = await new Detox(detoxContext).init();
|
||||
});
|
||||
|
||||
it(`should shut the device down on detox.cleanup()`, async () => {
|
||||
@@ -611,20 +556,21 @@ describe('Detox', () => {
|
||||
['onTestDone', testSummaries.passed()],
|
||||
['onRunDescribeFinish', { name: 'testSuiteName' }],
|
||||
['onRunFinish', null],
|
||||
])('when detox[symbols.%s](%j) is called', (method, arg) => {
|
||||
])('when DetoxWorker#@%s(%j) is called', (method, arg) => {
|
||||
beforeEach(async () => {
|
||||
detox = await new Detox(detoxConfig).init();
|
||||
detox = await new Detox(detoxContext).init();
|
||||
});
|
||||
|
||||
it(`should pass it through to artifactsManager.${method}()`, async () => {
|
||||
await detox[lifecycleSymbols[method]](arg);
|
||||
await detox[method](arg);
|
||||
expect(artifactsManager[method]).toHaveBeenCalledWith(arg);
|
||||
});
|
||||
});
|
||||
|
||||
describe('global context', () => {
|
||||
// TODO: move it to the root realm
|
||||
describe.skip('global context', () => {
|
||||
const configs = {
|
||||
deviceConfig: {
|
||||
device: {
|
||||
mock: 'config',
|
||||
},
|
||||
};
|
||||
@@ -648,7 +594,7 @@ describe('Detox', () => {
|
||||
|
||||
await Detox.globalInit(configs);
|
||||
expect(lifecycleHandler.globalInit).toHaveBeenCalled();
|
||||
expect(environmentFactory.createGlobalLifecycleHandler).toHaveBeenCalledWith(configs.deviceConfig);
|
||||
expect(environmentFactory.createGlobalLifecycleHandler).toHaveBeenCalledWith(configs.device);
|
||||
});
|
||||
|
||||
it(`should not invoke init if no handler was resolved`, async () => {
|
||||
@@ -661,7 +607,7 @@ describe('Detox', () => {
|
||||
|
||||
await Detox.globalCleanup(configs);
|
||||
expect(lifecycleHandler.globalCleanup).toHaveBeenCalled();
|
||||
expect(environmentFactory.createGlobalLifecycleHandler).toHaveBeenCalledWith(configs.deviceConfig);
|
||||
expect(environmentFactory.createGlobalLifecycleHandler).toHaveBeenCalledWith(configs.device);
|
||||
});
|
||||
|
||||
it(`should not invoke cleanup if no handler was resolved`, async () => {
|
||||
@@ -1,24 +1,72 @@
|
||||
const EventEmitter = require('events');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const _ = require('lodash');
|
||||
|
||||
const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
|
||||
const log = require('../utils/logger').child({ __filename });
|
||||
const log = require('../utils/logger').child({ cat: 'artifacts-manager,artifact' });
|
||||
const resolveModuleFromPath = require('../utils/resolveModuleFromPath');
|
||||
const traceMethods = require('../utils/traceMethods');
|
||||
|
||||
const FileArtifact = require('./templates/artifact/FileArtifact');
|
||||
const ArtifactPathBuilder = require('./utils/ArtifactPathBuilder');
|
||||
|
||||
class ArtifactsManager extends EventEmitter {
|
||||
constructor({ pathBuilder, plugins }) {
|
||||
constructor({ rootDir, pathBuilder, plugins }) {
|
||||
super();
|
||||
|
||||
this._pluginConfigs = plugins;
|
||||
this._idlePromise = Promise.resolve();
|
||||
this._idleCallbackRequests = [];
|
||||
this._artifactPlugins = {};
|
||||
this._pathBuilder = pathBuilder;
|
||||
this._pathBuilder = this._resolveArtifactsPathBuilder(pathBuilder, rootDir);
|
||||
|
||||
traceMethods(log, this, [
|
||||
'onAppReady',
|
||||
'onBeforeCleanup',
|
||||
'onBeforeLaunchApp',
|
||||
'onBeforeShutdownDevice',
|
||||
'onBeforeTerminateApp',
|
||||
'onBeforeUninstallApp',
|
||||
'onBootDevice',
|
||||
'onCreateExternalArtifact',
|
||||
'onHookFailure',
|
||||
'onHookStart',
|
||||
'onHookSuccess',
|
||||
'onLaunchApp',
|
||||
'onRunDescribeFinish',
|
||||
'onRunDescribeStart',
|
||||
'onRunFinish',
|
||||
'onRunStart',
|
||||
'onShutdownDevice',
|
||||
'onTerminateApp',
|
||||
'onTestDone',
|
||||
'onTestFnFailure',
|
||||
'onTestFnStart',
|
||||
'onTestFnSuccess',
|
||||
'onTestStart',
|
||||
]);
|
||||
}
|
||||
|
||||
_resolveArtifactsPathBuilder(pathBuilder, rootDir) {
|
||||
if (typeof pathBuilder === 'string') {
|
||||
pathBuilder = resolveModuleFromPath(pathBuilder);
|
||||
|
||||
if (typeof pathBuilder === 'function') {
|
||||
try {
|
||||
pathBuilder = pathBuilder({ rootDir });
|
||||
} catch (e) {
|
||||
pathBuilder = new pathBuilder({ rootDir });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!pathBuilder) {
|
||||
pathBuilder = new ArtifactPathBuilder({ rootDir });
|
||||
}
|
||||
|
||||
return pathBuilder;
|
||||
}
|
||||
|
||||
_instantiateArtifactPlugin(pluginFactory, pluginUserConfig) {
|
||||
@@ -179,27 +227,21 @@ class ArtifactsManager extends EventEmitter {
|
||||
}
|
||||
|
||||
async _callSinglePlugin(pluginId, methodName, ...args) {
|
||||
const callSignature = this._composeCallSignature('artifactsManager', methodName, args);
|
||||
log.trace(Object.assign({ event: 'ARTIFACTS_LIFECYCLE', fn: methodName }, ...args), callSignature);
|
||||
|
||||
const plugin = this._artifactPlugins[pluginId];
|
||||
try {
|
||||
await plugin[methodName](...args);
|
||||
} catch (e) {
|
||||
this._unhandledPluginExceptionHandler(e, { plugin, methodName, args });
|
||||
this._unhandledPluginExceptionHandler(e, { plugin, methodName });
|
||||
}
|
||||
}
|
||||
|
||||
async _callPlugins(strategy, methodName, ...args) {
|
||||
const callSignature = this._composeCallSignature('artifactsManager', methodName, args);
|
||||
log.trace(Object.assign({ event: 'ARTIFACTS_LIFECYCLE', fn: methodName }, ...args), callSignature);
|
||||
|
||||
for (const pluginGroup of this._groupPlugins(strategy)) {
|
||||
await Promise.all(pluginGroup.map(async (plugin) => {
|
||||
try {
|
||||
await plugin[methodName](...args);
|
||||
} catch (e) {
|
||||
this._unhandledPluginExceptionHandler(e, { plugin, methodName, args });
|
||||
this._unhandledPluginExceptionHandler(e, { plugin, methodName });
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -229,28 +271,20 @@ class ArtifactsManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_composeCallSignature(object, methodName, args) {
|
||||
const argsString = args.map(arg => util.inspect(arg)).join(', ');
|
||||
return `${object}.${methodName}(${argsString})`;
|
||||
}
|
||||
|
||||
_unhandledPluginExceptionHandler(err, { plugin, methodName, args }) {
|
||||
_unhandledPluginExceptionHandler(err, { plugin, methodName }) {
|
||||
const logObject = {
|
||||
event: 'ERROR',
|
||||
plugin: plugin.name,
|
||||
err,
|
||||
methodName,
|
||||
err,
|
||||
};
|
||||
|
||||
const callSignature = this._composeCallSignature(plugin.name, methodName, args);
|
||||
log.warn(logObject, `Suppressed error inside function call: ${callSignature}`);
|
||||
log.warn(logObject, `Suppressed error inside function call.`);
|
||||
}
|
||||
|
||||
_idleCallbackErrorHandle(err, caller) {
|
||||
this._unhandledPluginExceptionHandler(err, {
|
||||
plugin: caller,
|
||||
methodName: 'onIdleCallback',
|
||||
args: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,13 @@ const testHookError = () => ({ hook: 'beforeEach', error: new Error() });
|
||||
const testError = () => ({ error: new Error() });
|
||||
|
||||
describe('ArtifactsManager', () => {
|
||||
let proxy, FakePathBuilder;
|
||||
let proxy;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('./__mocks__/FakePathBuilder');
|
||||
jest.mock('./utils/ArtifactPathBuilder');
|
||||
jest.mock('../utils/argparse');
|
||||
jest.mock('../utils/logger');
|
||||
|
||||
FakePathBuilder = require('./__mocks__/FakePathBuilder');
|
||||
|
||||
proxy = {
|
||||
get ArtifactPathBuilder() {
|
||||
return require('./utils/ArtifactPathBuilder');
|
||||
@@ -32,9 +28,6 @@ describe('ArtifactsManager', () => {
|
||||
get fs() {
|
||||
return require('fs-extra');
|
||||
},
|
||||
get argparse() {
|
||||
return require('../utils/argparse');
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -103,9 +96,8 @@ describe('ArtifactsManager', () => {
|
||||
});
|
||||
};
|
||||
|
||||
pathBuilder = new FakePathBuilder();
|
||||
artifactsManager = new proxy.ArtifactsManager({
|
||||
pathBuilder,
|
||||
pathBuilder: path.join(__dirname, '__mocks__/FakePathBuilder.js'),
|
||||
plugins: {
|
||||
testPlugin: {
|
||||
lifecycle: 'all',
|
||||
@@ -113,6 +105,8 @@ describe('ArtifactsManager', () => {
|
||||
}
|
||||
});
|
||||
artifactsManager.registerArtifactPlugins({ testPlugin: testPluginFactory });
|
||||
pathBuilder = artifactsManager._pathBuilder;
|
||||
jest.spyOn(pathBuilder, 'buildPathForTestArtifact');
|
||||
});
|
||||
|
||||
describe('.userConfig', () => {
|
||||
@@ -236,11 +230,10 @@ describe('ArtifactsManager', () => {
|
||||
expect(proxy.logger.warn.mock.calls).toEqual([[
|
||||
{
|
||||
err: expect.any(Error),
|
||||
event: 'ERROR',
|
||||
methodName: hookName,
|
||||
plugin: 'testPlugin',
|
||||
},
|
||||
expect.stringContaining(`Suppressed error inside function call: testPlugin.${hookName}`)
|
||||
expect.stringContaining(`Suppressed error inside function call.`)
|
||||
]]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const fs = require('fs-extra');
|
||||
const _ = require('lodash');
|
||||
|
||||
const log = require('../../../utils/logger').child({ __filename });
|
||||
const log = require('../../../utils/logger').child({ cat: 'artifact' });
|
||||
const FileArtifact = require('../../templates/artifact/FileArtifact');
|
||||
const InstrumentsArtifactRecording = require('../InstrumentsArtifactRecording');
|
||||
|
||||
@@ -37,9 +37,9 @@ class SimulatorInstrumentsRecording extends InstrumentsArtifactRecording {
|
||||
SimulatorInstrumentsRecording.hintAboutDetoxInstruments = _.once(() => {
|
||||
log.warn(`Make sure either:
|
||||
1. You have installed Detox Instruments:
|
||||
https://github.com/wix/DetoxInstruments#installation
|
||||
https://github.com/wix/DetoxInstruments#installation
|
||||
2. You have integrated Detox Instruments in your app:
|
||||
https://github.com/wix/DetoxInstruments/blob/master/Documentation/XcodeIntegrationGuide.md
|
||||
https://github.com/wix/DetoxInstruments/blob/master/Documentation/XcodeIntegrationGuide.md
|
||||
3. You have set the environment variable with your custom Detox Instruments location:
|
||||
export DETOX_INSTRUMENTS_PATH="/path/to/Detox Instruments.app"`);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const childProcess = require('../../../utils/childProcess');
|
||||
const log = require('../../../utils/logger').child({ __filename });
|
||||
const log = require('../../../utils/logger').child({ cat: 'artifact' });
|
||||
const sleep = require('../../../utils/sleep');
|
||||
const Artifact = require('../../templates/artifact/Artifact');
|
||||
const FileArtifact = require('../../templates/artifact/FileArtifact');
|
||||
|
||||
@@ -12,25 +12,21 @@ class AndroidArtifactPluginsProvider extends ArtifactPluginsProvider {
|
||||
const ADBLogcatPlugin = require('../log/android/ADBLogcatPlugin');
|
||||
const ADBScreencapPlugin = require('../screenshot/ADBScreencapPlugin');
|
||||
const ADBScreenrecorderPlugin = require('../video/ADBScreenrecorderPlugin');
|
||||
const TimelineArtifactPlugin = require('../timeline/TimelineArtifactPlugin');
|
||||
|
||||
return {
|
||||
instruments: (api) => new AndroidInstrumentsPlugin({ api, adb, client, devicePathBuilder }),
|
||||
log: (api) => new ADBLogcatPlugin({ api, adb, devicePathBuilder }),
|
||||
screenshot: (api) => new ADBScreencapPlugin({ api, adb, devicePathBuilder }),
|
||||
video: (api) => new ADBScreenrecorderPlugin({ api, adb, devicePathBuilder }),
|
||||
timeline: (api) => new TimelineArtifactPlugin({ api }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class IosArtifactPluginsProvider extends ArtifactPluginsProvider {
|
||||
declareArtifactPlugins({ client }) {
|
||||
const TimelineArtifactPlugin = require('../timeline/TimelineArtifactPlugin');
|
||||
const IosUIHierarchyPlugin = require('../uiHierarchy/IosUIHierarchyPlugin');
|
||||
|
||||
return {
|
||||
timeline: (api) => new TimelineArtifactPlugin({ api }),
|
||||
uiHierarchy: (api) => new IosUIHierarchyPlugin({ api, client }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const path = require('path');
|
||||
|
||||
const fs = require('../../utils/fsext');
|
||||
const log = require('../../utils/logger').child({ __filename });
|
||||
const log = require('../../utils/logger').child({ cat: 'artifacts-plugin,artifacts' });
|
||||
const FileArtifact = require('../templates/artifact/FileArtifact');
|
||||
const temporaryPath = require('../utils/temporaryPath');
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
const log = require('../../../utils/logger').child({ __filename });
|
||||
const log = require('../../../utils/logger').child({ cat: 'artifact' });
|
||||
|
||||
class Artifact {
|
||||
constructor(template) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const log = require('../../../utils/logger').child({ __filename });
|
||||
const log = require('../../../utils/logger').child({ cat: 'artifacts-plugin,artifacts' });
|
||||
|
||||
/***
|
||||
* Almost non-opinionated building block for any artifact type
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
const fs = require('fs-extra');
|
||||
const _noop = require('lodash/noop');
|
||||
|
||||
const ChromeTracingExporter = require('../../utils/ChromeTracingExporter');
|
||||
const fakeTimestampsProvider = require('../../utils/fakeTimestampsProvider');
|
||||
const { trace } = require('../../utils/trace');
|
||||
const FileArtifact = require('../templates/artifact/FileArtifact');
|
||||
const ArtifactPlugin = require('../templates/plugin/ArtifactPlugin');
|
||||
|
||||
const traceNoop = {
|
||||
startSection: _noop,
|
||||
endSection: _noop,
|
||||
};
|
||||
|
||||
const CONTEXT_TYPES = {
|
||||
TEST: 'test',
|
||||
DESCRIBE: 'describe'
|
||||
};
|
||||
|
||||
class TimelineArtifactPlugin extends ArtifactPlugin {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this._useFakeTimestamps = config.useFakeTimestamps;
|
||||
|
||||
const threadId = process.env.JEST_WORKER_ID || process.pid;
|
||||
this._trace = this.enabled ? trace : traceNoop;
|
||||
this._traceExporter = new ChromeTracingExporter({
|
||||
process: { id: 0, name: 'detox' },
|
||||
thread: { id: threadId, name: `Worker #${threadId}` },
|
||||
});
|
||||
}
|
||||
|
||||
async onBootDevice(event) {
|
||||
this._deviceName = event.deviceId;
|
||||
return super.onBootDevice(event);
|
||||
}
|
||||
|
||||
async onRunDescribeStart(suite) {
|
||||
const sectionName = (suite.name === 'ROOT_DESCRIBE_BLOCK' ? this._deviceName : suite.name);
|
||||
this._trace.startSection(sectionName, { context: CONTEXT_TYPES.DESCRIBE });
|
||||
await super.onRunDescribeStart(suite);
|
||||
}
|
||||
|
||||
async onRunDescribeFinish(suite) {
|
||||
const sectionName = (suite.name === 'ROOT_DESCRIBE_BLOCK' ? this._deviceName : suite.name);
|
||||
this._trace.endSection(sectionName, { context: CONTEXT_TYPES.DESCRIBE });
|
||||
await super.onRunDescribeFinish(suite);
|
||||
}
|
||||
|
||||
async onTestStart(testSummary) {
|
||||
this._trace.startSection(testSummary.title, { context: CONTEXT_TYPES.TEST });
|
||||
await super.onTestStart(testSummary);
|
||||
}
|
||||
|
||||
async onTestDone(testSummary) {
|
||||
this._trace.endSection(testSummary.title, { status: testSummary.status, context: CONTEXT_TYPES.TEST });
|
||||
await super.onTestDone(testSummary);
|
||||
}
|
||||
|
||||
async onBeforeCleanup() {
|
||||
this._deviceName = null;
|
||||
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const traceLogPath = await this.api.preparePathForArtifact(`detox.trace.json`);
|
||||
const append = await this._logFileExists(traceLogPath);
|
||||
const events = this._useFakeTimestamps ? this._transformEventTimestamps(trace.events) : trace.events;
|
||||
const data = this._traceExporter.export(events, append);
|
||||
|
||||
const fileArtifact = new FileArtifact({ temporaryData: data });
|
||||
await fileArtifact.save(traceLogPath, { append });
|
||||
}
|
||||
|
||||
async _logFileExists(traceLogPath) {
|
||||
return fs.access(traceLogPath).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
_transformEventTimestamps(events) {
|
||||
return events.map((event) => ({
|
||||
...event,
|
||||
ts: fakeTimestampsProvider(),
|
||||
}));
|
||||
}
|
||||
|
||||
/** @param {string} config */
|
||||
static parseConfig(config) {
|
||||
return {
|
||||
enabled: config === 'all',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TimelineArtifactPlugin;
|
||||
@@ -1,313 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const latestInstanceOf = (clazz) => _.last(clazz.mock.instances);
|
||||
|
||||
describe('TimelineArtifactPlugin', () => {
|
||||
const configMock = ({ enabled = true } = {}) => ({
|
||||
api: {
|
||||
userConfig: {
|
||||
enabled,
|
||||
},
|
||||
preparePathForArtifact: async path => path,
|
||||
},
|
||||
});
|
||||
|
||||
let fs;
|
||||
let ChromeTracingExporter;
|
||||
let chromeTracingExporterObj;
|
||||
let FileArtifact;
|
||||
let fileArtifactObj;
|
||||
let trace;
|
||||
let TimelineArtifactPlugin;
|
||||
let uutEnabled;
|
||||
let uutDisabled;
|
||||
beforeEach(() => {
|
||||
jest.mock('fs-extra', () => ({
|
||||
access: jest.fn(),
|
||||
}));
|
||||
fs = require('fs-extra');
|
||||
|
||||
jest.mock('../../utils/ChromeTracingExporter');
|
||||
ChromeTracingExporter = require('../../utils/ChromeTracingExporter');
|
||||
chromeTracingExporterObj = () => latestInstanceOf(ChromeTracingExporter);
|
||||
|
||||
jest.mock('../templates/artifact/FileArtifact');
|
||||
FileArtifact = require('../templates/artifact/FileArtifact');
|
||||
fileArtifactObj = () => latestInstanceOf(FileArtifact);
|
||||
|
||||
const { Trace: MockTrace } = jest.genMockFromModule('../../utils/trace');
|
||||
const mockTrace = new MockTrace();
|
||||
jest.mock('../../utils/trace', () => ({
|
||||
trace: mockTrace,
|
||||
}));
|
||||
trace = mockTrace;
|
||||
|
||||
TimelineArtifactPlugin = require('./TimelineArtifactPlugin');
|
||||
uutEnabled = () => new TimelineArtifactPlugin(configMock({ enabled: true }));
|
||||
uutDisabled = () => new TimelineArtifactPlugin(configMock({ enabled: false }));
|
||||
});
|
||||
|
||||
describe('onBootDevice', () => {
|
||||
const deviceId = 'testDeviceId';
|
||||
|
||||
it('should set device ID into plugin\'s context', async () => {
|
||||
const uut = uutEnabled();
|
||||
await uut.onBootDevice({ deviceId });
|
||||
expect(uut.context.deviceId).toEqual(deviceId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Describe block hooks', () => {
|
||||
const suite = {
|
||||
name: 'suite-name',
|
||||
};
|
||||
|
||||
it('should trace a section-start with the suite name on describe-start', async () => {
|
||||
await uutEnabled().onRunDescribeStart(suite);
|
||||
expect(trace.startSection).toHaveBeenCalledWith(suite.name, { context: 'describe' });
|
||||
});
|
||||
|
||||
it('should save the suite in the plugin-context on describe-start', async () => {
|
||||
const uut = uutEnabled();
|
||||
await uut.onRunDescribeStart(suite);
|
||||
expect(uut.context.suite).toEqual(suite);
|
||||
});
|
||||
|
||||
it('should trace a section-end with the suite name on describe-end', async () => {
|
||||
const uut = uutEnabled();
|
||||
await uut.onRunDescribeStart(suite);
|
||||
await uut.onRunDescribeFinish(suite);
|
||||
expect(trace.endSection).toHaveBeenCalledWith(suite.name, { context: 'describe' });
|
||||
});
|
||||
|
||||
it('should not trace anything if disabled', async () => {
|
||||
const uut = uutDisabled();
|
||||
await uut.onRunDescribeStart(suite);
|
||||
await uut.onRunDescribeFinish(suite);
|
||||
expect(trace.startSection).not.toHaveBeenCalled();
|
||||
expect(trace.endSection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should clear the suite from the plugin\'s context on describe-end', async () => {
|
||||
const uut = uutEnabled();
|
||||
await uut.onRunDescribeStart(suite);
|
||||
await uut.onRunDescribeFinish(suite);
|
||||
expect(uut.context.suite).toBeNull();
|
||||
});
|
||||
|
||||
describe('For the root describe block', () => {
|
||||
const deviceId = 'deviceID-mock';
|
||||
const rootSuite = {
|
||||
name: 'ROOT_DESCRIBE_BLOCK',
|
||||
};
|
||||
|
||||
it('should trace the root-section\'s start with the _device_ name', async () => {
|
||||
const uut = uutEnabled();
|
||||
await uut.onBootDevice({ deviceId });
|
||||
await uut.onRunDescribeStart(rootSuite);
|
||||
expect(trace.startSection).toHaveBeenCalledWith(deviceId, { context: 'describe' });
|
||||
expect(trace.startSection).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should trace the root-section\'s end with the _device_ name', async () => {
|
||||
const uut = uutEnabled();
|
||||
await uut.onBootDevice({ deviceId });
|
||||
await uut.onRunDescribeStart(rootSuite);
|
||||
await uut.onRunDescribeFinish(rootSuite);
|
||||
expect(trace.endSection).toHaveBeenCalledWith(deviceId, { context: 'describe' });
|
||||
expect(trace.endSection).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test block hooks', () => {
|
||||
const testSummary = {
|
||||
title: 'test-name-mock',
|
||||
};
|
||||
|
||||
it('should trace a section-start with the test name on test-start', async () => {
|
||||
const uut = uutEnabled();
|
||||
await uut.onTestStart(testSummary);
|
||||
expect(trace.startSection).toHaveBeenCalledWith(testSummary.title, { context: 'test' });
|
||||
});
|
||||
|
||||
it('should save the test summary in the plugin\'s context on test-start', async () => {
|
||||
const uut = uutEnabled();
|
||||
await uut.onTestStart(testSummary);
|
||||
expect(uut.context.testSummary).toEqual(testSummary);
|
||||
});
|
||||
|
||||
it('should trace a section-end with the test name on test-end', async () => {
|
||||
const _testSummary = { ...testSummary, status: 'alright!' };
|
||||
await uutEnabled().onTestDone(_testSummary);
|
||||
expect(trace.endSection).toHaveBeenCalledWith(testSummary.title, { status: 'alright!', context: 'test' });
|
||||
});
|
||||
|
||||
it('should not trace anything if disabled', async () => {
|
||||
const _testSummary = { ...testSummary, status: 'alright!' };
|
||||
const uut = uutDisabled();
|
||||
await uut.onTestStart(testSummary);
|
||||
await uut.onTestDone(_testSummary);
|
||||
expect(trace.startSection).not.toHaveBeenCalled();
|
||||
expect(trace.endSection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save the test summary from the plugin\'s context on test-end', async () => {
|
||||
const _testSummary = { ...testSummary, status: 'alright!' };
|
||||
const uut = uutEnabled();
|
||||
await uut.onTestDone(_testSummary);
|
||||
expect(uut.context.testSummary).toEqual(_testSummary);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onBeforeCleanup', () => {
|
||||
const expectedArtifactPath = `detox.trace.json`;
|
||||
|
||||
const givenArtifactFileNotExists = () => fs.access.mockImplementation(async () => {
|
||||
throw new Error('Make uutEnabled think the file doesnt already exist');
|
||||
});
|
||||
const givenArtifactFileAlreadyExists = () => fs.access.mockResolvedValue(undefined);
|
||||
const givenExportedTraceDataResult = (result) => chromeTracingExporterObj().export.mockReturnValue(result);
|
||||
|
||||
it('should create artifact with exported trace data', async () => {
|
||||
const uut = uutEnabled();
|
||||
const exportedData = JSON.stringify({ mocked: 'mocked data' });
|
||||
givenArtifactFileNotExists();
|
||||
givenExportedTraceDataResult(exportedData);
|
||||
|
||||
await uut.onBeforeCleanup();
|
||||
|
||||
expect(FileArtifact).toHaveBeenCalledWith({ temporaryData: exportedData });
|
||||
expect(fileArtifactObj().save).toHaveBeenCalledWith(expectedArtifactPath, { append: false });
|
||||
});
|
||||
|
||||
it('should append exported data if artifact file already exists', async () => {
|
||||
const uut = uutEnabled();
|
||||
const exportedData = JSON.stringify({ mocked: 'mocked data' });
|
||||
givenArtifactFileAlreadyExists();
|
||||
givenExportedTraceDataResult(exportedData);
|
||||
|
||||
await uut.onBeforeCleanup();
|
||||
|
||||
expect(fileArtifactObj().save).toHaveBeenCalledWith(expectedArtifactPath, { append: true });
|
||||
});
|
||||
|
||||
it('should not save artifact if disabled', async () => {
|
||||
const uut = uutDisabled();
|
||||
const exportedData = JSON.stringify({ mocked: 'mocked data' });
|
||||
givenArtifactFileNotExists();
|
||||
givenExportedTraceDataResult(exportedData);
|
||||
|
||||
await uut.onBeforeCleanup();
|
||||
|
||||
expect(chromeTracingExporterObj().export).not.toHaveBeenCalled();
|
||||
expect(fileArtifactObj()).toBeUndefined();
|
||||
expect(fs.access).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should properly init the trace-events exporter, and use the jest worker ID for thread-ID', async () => {
|
||||
process.env.JEST_WORKER_ID = '102030';
|
||||
const expectedThreadId = '102030';
|
||||
const expectedThreadName = `Worker #102030`;
|
||||
const uut = uutEnabled();
|
||||
|
||||
const exportedData = JSON.stringify({ mocked: 'mocked data' });
|
||||
givenArtifactFileNotExists();
|
||||
givenExportedTraceDataResult(exportedData);
|
||||
|
||||
await uut.onBeforeCleanup();
|
||||
|
||||
expect(ChromeTracingExporter).toHaveBeenCalledWith({
|
||||
process: { id: 0, name: 'detox' },
|
||||
thread: { id: expectedThreadId, name: expectedThreadName },
|
||||
});
|
||||
});
|
||||
|
||||
it('should resort to our pid as the exporter\'s thread-ID if not running using Jest', async () => {
|
||||
process.env.JEST_WORKER_ID = '';
|
||||
const expectedThreadId = process.pid;
|
||||
const expectedThreadName = `Worker #${process.pid}`;
|
||||
const uut = uutEnabled();
|
||||
|
||||
const exportedData = JSON.stringify({ mocked: 'mocked data' });
|
||||
givenArtifactFileNotExists();
|
||||
givenExportedTraceDataResult(exportedData);
|
||||
|
||||
await uut.onBeforeCleanup();
|
||||
|
||||
expect(ChromeTracingExporter).toHaveBeenCalledWith(expect.objectContaining({
|
||||
thread: { id: expectedThreadId, name: expectedThreadName },
|
||||
}));
|
||||
});
|
||||
|
||||
it('should use trace events as inpute to data exporter', async () => {
|
||||
trace.events = [
|
||||
{ name: 'mock-event', type: 'start', ts: 1234 },
|
||||
{ name: 'mock-event', type: 'end', ts: 1235 },
|
||||
];
|
||||
const uut = uutEnabled();
|
||||
|
||||
givenArtifactFileNotExists();
|
||||
givenExportedTraceDataResult('mock');
|
||||
|
||||
await uut.onBeforeCleanup();
|
||||
|
||||
expect(chromeTracingExporterObj().export).toHaveBeenCalledWith(trace.events, false);
|
||||
});
|
||||
|
||||
it('should pass in append=true to exporter if artifact file already exists', async () => {
|
||||
trace.events = [];
|
||||
const uut = uutEnabled();
|
||||
|
||||
givenArtifactFileAlreadyExists();
|
||||
givenExportedTraceDataResult('mock');
|
||||
|
||||
await uut.onBeforeCleanup();
|
||||
|
||||
expect(chromeTracingExporterObj().export).toHaveBeenCalledWith([], true);
|
||||
});
|
||||
|
||||
describe('In a stub-like operational mode', () => {
|
||||
let uut;
|
||||
beforeEach(() => {
|
||||
uut = new TimelineArtifactPlugin({
|
||||
...configMock({ enabled: true }),
|
||||
useFakeTimestamps: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should rewrite the timeline events with fake (deterministic) timestamps', async () => {
|
||||
const events = [
|
||||
{ type: 'init', ts: 1233 },
|
||||
{ name: 'mock-event', type: 'start', ts: 1234 },
|
||||
{ name: 'mock-event', type: 'end', ts: 1235 },
|
||||
];
|
||||
const expectedEvents = [
|
||||
{ type: 'init', ts: 1000 },
|
||||
{ name: 'mock-event', type: 'start', ts: 1100 },
|
||||
{ name: 'mock-event', type: 'end', ts: 1200 },
|
||||
];
|
||||
trace.events = events;
|
||||
|
||||
givenArtifactFileNotExists();
|
||||
givenExportedTraceDataResult('mock');
|
||||
|
||||
await uut.onBeforeCleanup();
|
||||
|
||||
expect(chromeTracingExporterObj().export).toHaveBeenCalledWith(expectedEvents, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseConfig', () => {
|
||||
let parseConfig;
|
||||
beforeEach(() => {
|
||||
parseConfig = TimelineArtifactPlugin.parseConfig;
|
||||
});
|
||||
|
||||
it('should enable plugin if config = all', () =>
|
||||
expect(parseConfig('all').enabled).toBe(true));
|
||||
|
||||
it('should disable plugin if config != all', () =>
|
||||
expect(parseConfig('none').enabled).toBe(false));
|
||||
});
|
||||
});
|
||||
@@ -4,12 +4,10 @@ const getTimeStampString = require('./getTimeStampString');
|
||||
|
||||
function buildDefaultRootForArtifactsRootDirpath(configuration, rootDir) {
|
||||
if (rootDir.endsWith('/') || rootDir.endsWith('\\')) {
|
||||
return rootDir;
|
||||
return rootDir.slice(0, -1);
|
||||
}
|
||||
|
||||
const seed = Number(process.env.DETOX_START_TIMESTAMP || String(Date.now()));
|
||||
const subdir = `${configuration}.${getTimeStampString(new Date(seed))}`;
|
||||
return path.join(rootDir, subdir);
|
||||
return path.join(rootDir, `${configuration}.${getTimeStampString(new Date())}`);
|
||||
}
|
||||
|
||||
module.exports = buildDefaultRootForArtifactsRootDirpath;
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
jest.useFakeTimers();
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const buildDefaultArtifactsRootDirpath = require('./buildDefaultArtifactsRootDirpath');
|
||||
const getTimeStampString = require('./getTimeStampString');
|
||||
|
||||
describe('buildDefaultArtifactsRootDirpath', () => {
|
||||
let buildDefaultArtifactsRootDirpath;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.DETOX_START_TIMESTAMP = '1572951641499';
|
||||
buildDefaultArtifactsRootDirpath = require('./buildDefaultArtifactsRootDirpath');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete process.env.DETOX_START_TIMESTAMP;
|
||||
});
|
||||
|
||||
it('should append subdir if dir does not end with /', () => {
|
||||
expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts')).toBe(path.join('artifacts', 'iphone8.2019-11-05 11-00-41Z'));
|
||||
});
|
||||
|
||||
it('should fall back to Date.now if environment variable DETOX_START_TIMESTAMP is unset', () => {
|
||||
delete process.env.DETOX_START_TIMESTAMP;
|
||||
expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts')).toMatch(/^artifacts[\\/]iphone8\.\d{4}-\d{2}-\d{2} \d{2}-\d{2}-\d{2}Z$/);
|
||||
expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts')).not.toBe(path.join('artifacts', 'iphone8.2019-11-05 11-00-41Z'));
|
||||
it('should append "<configurationName>.YYYY-MM-DD HH-MM-SSZ" subdir if dir does not end with /', () => {
|
||||
expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts')).toBe(path.join('artifacts', `iphone8.${getTimeStampString()}`));
|
||||
});
|
||||
|
||||
it('should not append subdir if dir ends with /', () => {
|
||||
expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts/')).toBe('artifacts/');
|
||||
expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts/')).toBe('artifacts');
|
||||
});
|
||||
|
||||
it('should not append subdir if dir ends with \\', () => {
|
||||
expect(buildDefaultArtifactsRootDirpath('iphone8', 'artifacts\\')).toBe('artifacts');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,13 +2,26 @@ const path = require('path');
|
||||
|
||||
const tempfile = require('tempfile');
|
||||
|
||||
function createTempFileBuilderFn(fileExtension) {
|
||||
/**
|
||||
* @param {string} [basename]
|
||||
*/
|
||||
return (basename) => {
|
||||
return basename
|
||||
? path.join(path.dirname(tempfile()), `${basename}.detox.${fileExtension}`)
|
||||
: tempfile(`.detox.${fileExtension}`);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
for: {
|
||||
png: () => tempfile('.detox.png'),
|
||||
log: () => tempfile('.detox.log'),
|
||||
mp4: () => tempfile('.detox.mp4'),
|
||||
dtxrec: () => tempfile('.detox.dtxrec'),
|
||||
viewhierarchy: () => tempfile('.detox.viewhierarchy'),
|
||||
json: createTempFileBuilderFn('json'),
|
||||
jsonl: createTempFileBuilderFn('jsonl'),
|
||||
png: createTempFileBuilderFn('png'),
|
||||
log: createTempFileBuilderFn('log'),
|
||||
mp4: createTempFileBuilderFn('mp4'),
|
||||
dtxrec: createTempFileBuilderFn('dtxrec'),
|
||||
viewhierarchy: createTempFileBuilderFn('viewhierarchy'),
|
||||
},
|
||||
mask: () => path.join(tempfile(), '..') + path.sep + '*.detox.*',
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const { interruptProcess } = require('../../utils/childProcess');
|
||||
const log = require('../../utils/logger').child({ __filename });
|
||||
const log = require('../../utils/logger').child({ cat: 'artifacts-plugin,artifact' });
|
||||
const Artifact = require('../templates/artifact/Artifact');
|
||||
const FileArtifact = require('../templates/artifact/FileArtifact');
|
||||
const temporaryPath = require('../utils/temporaryPath');
|
||||
|
||||
@@ -5,25 +5,16 @@ const WebSocket = require('ws');
|
||||
const DetoxInternalError = require('../errors/DetoxInternalError');
|
||||
const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
|
||||
const Deferred = require('../utils/Deferred');
|
||||
const log = require('../utils/logger').child({ __filename });
|
||||
const log = require('../utils/logger').child({ cat: 'ws-client,ws' });
|
||||
|
||||
const InflightRequest = require('./InflightRequest');
|
||||
|
||||
const EVENTS = {
|
||||
OPEN: Object.freeze({ event: 'WS_OPEN' }),
|
||||
ERROR: Object.freeze({ event: 'WS_ERROR' }),
|
||||
MESSAGE: Object.freeze({ event: 'WS_MESSAGE' }),
|
||||
SEND: Object.freeze({ event: 'WS_SEND' }),
|
||||
LATE_RESPONSE: Object.freeze({ event: 'WS_LATE_RESPONSE' }),
|
||||
};
|
||||
|
||||
const DEFAULT_SEND_OPTIONS = {
|
||||
timeout: 0,
|
||||
};
|
||||
|
||||
class AsyncWebSocket {
|
||||
constructor(url) {
|
||||
this._log = log.child({ url });
|
||||
this._url = url;
|
||||
this._ws = null;
|
||||
this._eventCallbacks = {};
|
||||
@@ -99,9 +90,9 @@ class AsyncWebSocket {
|
||||
|
||||
this.handleMultipleNonAtomicPendingActions();
|
||||
|
||||
const messageAsString = JSON.stringify(message);
|
||||
this._log.trace(EVENTS.SEND, messageAsString);
|
||||
this._ws.send(messageAsString);
|
||||
const payload = JSON.stringify(message);
|
||||
log.trace({ data: payload }, 'send message');
|
||||
this._ws.send(payload);
|
||||
|
||||
return inFlight.promise;
|
||||
}
|
||||
@@ -157,7 +148,7 @@ class AsyncWebSocket {
|
||||
}
|
||||
|
||||
if (!hasPendingActions) {
|
||||
log.error(EVENTS.ERROR, DetoxRuntimeError.format(error));
|
||||
log.error({ error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +177,7 @@ class AsyncWebSocket {
|
||||
* @private
|
||||
*/
|
||||
_onOpen(event) { // eslint-disable-line no-unused-vars
|
||||
this._log.trace(EVENTS.OPEN, `opened web socket to: ${this._url}`);
|
||||
log.trace(`opened web socket to: ${this._url}`);
|
||||
this._opening.resolve();
|
||||
this._opening = null;
|
||||
}
|
||||
@@ -234,7 +225,7 @@ class AsyncWebSocket {
|
||||
const data = event && event.data || null;
|
||||
|
||||
try {
|
||||
this._log.trace(EVENTS.MESSAGE, data);
|
||||
log.trace({ data }, 'get message');
|
||||
|
||||
const json = JSON.parse(data);
|
||||
if (!json || !json.type) {
|
||||
@@ -259,7 +250,7 @@ class AsyncWebSocket {
|
||||
|
||||
if (!handled) {
|
||||
if (this._abortedMessageIds.has(json.messageId)) {
|
||||
log.debug(EVENTS.LATE_RESPONSE, `Received late response for messageId=${json.messageId}`);
|
||||
log.debug({ messageId: json.messageId }, `late response`);
|
||||
} else {
|
||||
throw new DetoxRuntimeError('Unexpected message received over the web socket: ' + json.type);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ const _ = require('lodash');
|
||||
|
||||
const config = require('../configuration/configurations.mock').validSession;
|
||||
|
||||
// TODO: check why it triggers MaxListenersExceededWarning
|
||||
|
||||
describe('AsyncWebSocket', () => {
|
||||
let AsyncWebSocket;
|
||||
let WebSocket;
|
||||
@@ -19,6 +21,7 @@ describe('AsyncWebSocket', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mock('../utils/logger');
|
||||
jest.mock('../utils/trace');
|
||||
jest.mock('ws');
|
||||
WebSocket = require('ws');
|
||||
WebSocket.CONNECTING = 0;
|
||||
@@ -26,6 +29,7 @@ describe('AsyncWebSocket', () => {
|
||||
WebSocket.CLOSING = 2;
|
||||
WebSocket.CLOSED = 3;
|
||||
|
||||
WebSocket.prototype._socket = { localPort: NaN };
|
||||
WebSocket.prototype.readyState = WebSocket.CONNECTING;
|
||||
WebSocket.prototype.mockOpen = function () {
|
||||
this.readyState = WebSocket.OPEN;
|
||||
@@ -165,12 +169,9 @@ describe('AsyncWebSocket', () => {
|
||||
socket.mockMessage({ type: 'somethingElse' });
|
||||
|
||||
expect(onErrorCallback).not.toHaveBeenCalled();
|
||||
expect(log.error).toHaveBeenCalledWith(
|
||||
{ event: 'WS_ERROR' },
|
||||
expect.stringMatching('Unexpected error')
|
||||
);
|
||||
expect(log.error).toHaveBeenCalledWith({ error: expect.anything() });
|
||||
|
||||
const error = log.error.mock.calls[0][1];
|
||||
const { error } = log.error.mock.calls[0][0];
|
||||
expect(error).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -326,7 +327,7 @@ describe('AsyncWebSocket', () => {
|
||||
aws.resetInFlightPromises();
|
||||
|
||||
socket.mockMessage({ type: 'someReply', messageId: 1 });
|
||||
expect(log.debug).toHaveBeenCalledWith({ event: 'WS_LATE_RESPONSE' }, expect.stringContaining('messageId=1'));
|
||||
expect(log.debug).toHaveBeenCalledWith({ messageId: 1 }, 'late response');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
|
||||
const failedToReachTheApp = require('../errors/longreads/failedToReachTheApp');
|
||||
const Deferred = require('../utils/Deferred');
|
||||
const { asError, createErrorWithUserStack, replaceErrorStack } = require('../utils/errorUtils');
|
||||
const log = require('../utils/logger').child({ __filename });
|
||||
const log = require('../utils/logger').child({ cat: 'ws-client,ws' });
|
||||
|
||||
const AsyncWebSocket = require('./AsyncWebSocket');
|
||||
const actions = require('./actions/actions');
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('Client', () => {
|
||||
|
||||
jest.mock('../utils/logger');
|
||||
log = require('../utils/logger');
|
||||
log.level.mockReturnValue('debug');
|
||||
log._level.mockReturnValue('debug');
|
||||
|
||||
const AsyncWebSocket = jest.genMockFromModule('./AsyncWebSocket');
|
||||
mockAws = new AsyncWebSocket();
|
||||
@@ -503,7 +503,7 @@ describe('Client', () => {
|
||||
['debug'],
|
||||
['trace'],
|
||||
])(`should throw "testFailed" error with view hierarchy (on --loglevel %s)`, async (loglevel) => {
|
||||
log.level.mockReturnValue(loglevel);
|
||||
log._level.mockReturnValue(loglevel);
|
||||
mockAws.mockResponse('testFailed', { details: 'this is an error', viewHierarchy: 'mock-hierarchy' });
|
||||
await expect(client.execute(anInvocation)).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
@@ -513,7 +513,7 @@ describe('Client', () => {
|
||||
['warn'],
|
||||
['info'],
|
||||
])(`should throw "testFailed" error without view hierarchy but with a hint (on --loglevel %s)`, async (loglevel) => {
|
||||
log.level.mockReturnValue(loglevel);
|
||||
log._level.mockReturnValue(loglevel);
|
||||
mockAws.mockResponse('testFailed', { details: 'this is an error', viewHierarchy: 'mock-hierarchy' });
|
||||
const executionPromise = client.execute(anInvocation);
|
||||
await expect(executionPromise).rejects.toThrowErrorMatchingSnapshot();
|
||||
|
||||
@@ -7,7 +7,7 @@ FakeClient.setInfiniteConnect = () => {
|
||||
client.deferred = new Deferred();
|
||||
client.connect.mockReturnValue(client.deferred.promise);
|
||||
client.cleanup.mockImplementation(() => {
|
||||
client.deferred.reject('Fake error: aborted connection');
|
||||
client.deferred.reject(new Error('Fake error: aborted connection'));
|
||||
});
|
||||
|
||||
return client;
|
||||
|
||||
@@ -76,7 +76,7 @@ The tester has not received a response within 5000ms timeout to the message:
|
||||
`;
|
||||
|
||||
exports[`AsyncWebSocket .send() when opened should log an error if the incoming message was completely unexpected 1`] = `
|
||||
"Unexpected error on an attempt to handle the response received over the web socket.
|
||||
[DetoxRuntimeError: Unexpected error on an attempt to handle the response received over the web socket.
|
||||
|
||||
HINT: Examine the inner error:
|
||||
|
||||
@@ -84,7 +84,7 @@ Unexpected message received over the web socket: somethingElse
|
||||
|
||||
The payload was:
|
||||
|
||||
{\\"type\\":\\"somethingElse\\"}"
|
||||
{"type":"somethingElse"}]
|
||||
`;
|
||||
|
||||
exports[`AsyncWebSocket .send() when opened should reject all messages in the flight if there's an error 1`] = `
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
const { DetoxInternalError, DetoxRuntimeError } = require('../../errors');
|
||||
const { getDetoxLevel } = require('../../utils/logger');
|
||||
const logger = require('../../utils/logger');
|
||||
const formatJSONStatus = require('../actions/formatters/SyncStatusFormatter');
|
||||
|
||||
class Action {
|
||||
@@ -201,7 +201,7 @@ class Invoke extends Action {
|
||||
|
||||
if (response.params.viewHierarchy) {
|
||||
/* istanbul ignore next */
|
||||
if (/^(debug|trace)$/.test(getDetoxLevel())) {
|
||||
if (/^(debug|trace)$/.test(logger.level)) {
|
||||
debugInfo = 'View Hierarchy:\n' + response.params.viewHierarchy;
|
||||
} else {
|
||||
hint = 'To print view hierarchy on failed actions/matches, use log-level verbose or higher.';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`composeRunnerConfig unhappy scenarios deprecated "testRunner" in local config 1`] = `
|
||||
"testRunner should be an object, not a undefined
|
||||
|
||||
HINT: Check that in your Detox config.
|
||||
|
||||
{
|
||||
configurations: {
|
||||
default: {
|
||||
testRunner: 'nyc jest'
|
||||
}
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`composeRunnerConfig unhappy scenarios non-string and non-object "testRunner" in global config 1`] = `
|
||||
"testRunner should be an object, not a number
|
||||
|
||||
HINT: Check that in your Detox config.
|
||||
|
||||
{
|
||||
testRunner: 0,
|
||||
configurations: [Object]
|
||||
}"
|
||||
`;
|
||||
@@ -1,7 +1,7 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const argparse = require('../utils/argparse');
|
||||
const log = require('../utils/logger').child({ __filename });
|
||||
const log = require('../utils/logger').child({ cat: 'config' });
|
||||
|
||||
const { DEVICE_LAUNCH_ARGS_GENERIC_DEPRECATION } = require('./utils/warnings');
|
||||
|
||||
@@ -36,7 +36,7 @@ const deprecateDeviceLaunchArgs = (value) => {
|
||||
function collectCliConfig({ argv }) {
|
||||
const env = (key) => argparse.getArgValue(key);
|
||||
const get = (key, fallback) => {
|
||||
const value = argv ? argv[key] : env(key);
|
||||
const value = argv && Reflect.has(argv, key) ? argv[key] : env(key);
|
||||
return value === undefined ? fallback : value;
|
||||
};
|
||||
|
||||
@@ -47,7 +47,6 @@ function collectCliConfig({ argv }) {
|
||||
takeScreenshots: get('take-screenshots'),
|
||||
recordVideos: get('record-videos'),
|
||||
recordPerformance: get('record-performance'),
|
||||
recordTimeline: get('record-timeline'),
|
||||
cleanup: asBoolean(get('cleanup')),
|
||||
configPath: get('config-path'),
|
||||
configuration: get('configuration'),
|
||||
@@ -64,9 +63,8 @@ function collectCliConfig({ argv }) {
|
||||
loglevel: get('loglevel'),
|
||||
noColor: asBoolean(get('no-color')),
|
||||
reuse: asBoolean(get('reuse')),
|
||||
runnerConfig: get('runner-config'),
|
||||
useCustomLogger: asBoolean(get('use-custom-logger')),
|
||||
workers: asNumber(get('workers')),
|
||||
retries: asNumber(get('retries')),
|
||||
inspectBrk: asBoolean(get('inspect-brk')),
|
||||
}, _.isUndefined);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ describe('collectCliConfig', () => {
|
||||
...asString( ['takeScreenshots', 'DETOX_TAKE_SCREENSHOTS', 'take-screenshots']),
|
||||
...asString( ['recordVideos', 'DETOX_RECORD_VIDEOS', 'record-videos']),
|
||||
...asString( ['recordPerformance', 'DETOX_RECORD_PERFORMANCE', 'record-performance']),
|
||||
...asString( ['recordTimeline', 'DETOX_RECORD_TIMELINE', 'record-timeline']),
|
||||
...asBoolean(['cleanup', 'DETOX_CLEANUP', 'cleanup']),
|
||||
...asString( ['configPath', 'DETOX_CONFIG_PATH', 'config-path']),
|
||||
...asString( ['configuration' , 'DETOX_CONFIGURATION', 'configuration']),
|
||||
@@ -89,10 +88,9 @@ describe('collectCliConfig', () => {
|
||||
...asString( ['loglevel', 'DETOX_LOGLEVEL', 'loglevel']),
|
||||
...asBoolean(['noColor', 'DETOX_NO_COLOR', 'no-color']),
|
||||
...asBoolean(['reuse', 'DETOX_REUSE', 'reuse']),
|
||||
...asNumber( ['retries', 'DETOX_RETRIES', 'retries']),
|
||||
...asBoolean(['readonlyEmu', 'DETOX_READ_ONLY_EMU', null]),
|
||||
...asString( ['runnerConfig', 'DETOX_RUNNER_CONFIG', 'runner-config']),
|
||||
...asBoolean(['useCustomLogger', 'DETOX_USE_CUSTOM_LOGGER', 'use-custom-logger']),
|
||||
...asNumber( ['workers', 'DETOX_WORKERS', 'workers']),
|
||||
...asBoolean(['inspectBrk', 'DETOX_INSPECT_BRK', 'inspect-brk']),
|
||||
])('.%s property' , (key, envName, argName, input, expected) => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -4,12 +4,9 @@ const _ = require('lodash');
|
||||
const InstrumentsArtifactPlugin = require('../artifacts/instruments/InstrumentsArtifactPlugin');
|
||||
const LogArtifactPlugin = require('../artifacts/log/LogArtifactPlugin');
|
||||
const ScreenshotArtifactPlugin = require('../artifacts/screenshot/ScreenshotArtifactPlugin');
|
||||
const TimelineArtifactPlugin = require('../artifacts/timeline/TimelineArtifactPlugin');
|
||||
const IosUIHierarchyPlugin = require('../artifacts/uiHierarchy/IosUIHierarchyPlugin');
|
||||
const ArtifactPathBuilder = require('../artifacts/utils/ArtifactPathBuilder');
|
||||
const buildDefaultArtifactsRootDirpath = require('../artifacts/utils/buildDefaultArtifactsRootDirpath');
|
||||
const VideoArtifactPlugin = require('../artifacts/video/VideoArtifactPlugin');
|
||||
const resolveModuleFromPath = require('../utils/resolveModuleFromPath');
|
||||
|
||||
/**
|
||||
* @param {*} cliConfig
|
||||
@@ -31,7 +28,6 @@ function composeArtifactsConfig({
|
||||
screenshot: cliConfig.takeScreenshots,
|
||||
video: cliConfig.recordVideos,
|
||||
instruments: cliConfig.recordPerformance,
|
||||
timeline: cliConfig.recordTimeline,
|
||||
uiHierarchy: cliConfig.captureViewHierarchy,
|
||||
},
|
||||
}),
|
||||
@@ -40,13 +36,15 @@ function composeArtifactsConfig({
|
||||
extendArtifactsConfig(false),
|
||||
);
|
||||
|
||||
if (!artifactsConfig.pathBuilder) {
|
||||
artifactsConfig.pathBuilder = undefined;
|
||||
}
|
||||
|
||||
artifactsConfig.rootDir = buildDefaultArtifactsRootDirpath(
|
||||
configurationName,
|
||||
artifactsConfig.rootDir
|
||||
);
|
||||
|
||||
artifactsConfig.pathBuilder = resolveArtifactsPathBuilder(artifactsConfig);
|
||||
|
||||
return artifactsConfig;
|
||||
}
|
||||
|
||||
@@ -54,13 +52,12 @@ function extendArtifactsConfig(config) {
|
||||
if (config === false) {
|
||||
return extendArtifactsConfig({
|
||||
rootDir: 'artifacts',
|
||||
pathBuilder: null,
|
||||
pathBuilder: '',
|
||||
plugins: {
|
||||
log: 'none',
|
||||
screenshot: 'manual',
|
||||
video: 'none',
|
||||
instruments: 'none',
|
||||
timeline: 'none',
|
||||
uiHierarchy: 'disabled',
|
||||
},
|
||||
});
|
||||
@@ -79,34 +76,11 @@ function extendArtifactsConfig(config) {
|
||||
screenshot: ifString(p.screenshot, ScreenshotArtifactPlugin.parseConfig),
|
||||
video: ifString(p.video, VideoArtifactPlugin.parseConfig),
|
||||
instruments: ifString(p.instruments, InstrumentsArtifactPlugin.parseConfig),
|
||||
timeline: ifString(p.timeline, TimelineArtifactPlugin.parseConfig),
|
||||
uiHierarchy: ifString(p.uiHierarchy, IosUIHierarchyPlugin.parseConfig),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolveArtifactsPathBuilder(artifactsConfig) {
|
||||
let { rootDir, pathBuilder } = artifactsConfig;
|
||||
|
||||
if (typeof pathBuilder === 'string') {
|
||||
pathBuilder = resolveModuleFromPath(pathBuilder);
|
||||
}
|
||||
|
||||
if (typeof pathBuilder === 'function') {
|
||||
try {
|
||||
pathBuilder = pathBuilder({ rootDir });
|
||||
} catch (e) {
|
||||
pathBuilder = new pathBuilder({ rootDir });
|
||||
}
|
||||
}
|
||||
|
||||
if (!pathBuilder) {
|
||||
pathBuilder = new ArtifactPathBuilder({ rootDir });
|
||||
}
|
||||
|
||||
return pathBuilder;
|
||||
}
|
||||
|
||||
function ifString(value, mapper) {
|
||||
return typeof value === 'string' ? mapper(value) : value;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
const path = require('path');
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const schemes = require('./configurations.mock');
|
||||
|
||||
describe('composeArtifactsConfig', () => {
|
||||
@@ -18,9 +14,7 @@ describe('composeArtifactsConfig', () => {
|
||||
globalConfig: {},
|
||||
cliConfig: {},
|
||||
})).toMatchObject({
|
||||
pathBuilder: expect.objectContaining({
|
||||
rootDir: expect.stringMatching(/^artifacts[\\/]abracadabra\.\d{4}/),
|
||||
}),
|
||||
rootDir: expect.stringMatching(/^artifacts[\\/]abracadabra\.\d{4}/),
|
||||
plugins: schemes.pluginsDefaultsResolved,
|
||||
});
|
||||
});
|
||||
@@ -32,15 +26,14 @@ describe('composeArtifactsConfig', () => {
|
||||
artifacts: {
|
||||
...schemes.allArtifactsConfiguration,
|
||||
rootDir: 'otherPlace',
|
||||
pathBuilder: _.noop,
|
||||
pathBuilder: '@local/pathbuilder',
|
||||
}
|
||||
},
|
||||
globalConfig: {},
|
||||
cliConfig: {},
|
||||
})).toMatchObject({
|
||||
pathBuilder: expect.objectContaining({
|
||||
rootDir: expect.stringMatching(/^otherPlace[\\/]abracadabra\.\d{4}/),
|
||||
}),
|
||||
rootDir: expect.stringMatching(/^otherPlace[\\/]abracadabra\.\d{4}/),
|
||||
pathBuilder: '@local/pathbuilder',
|
||||
plugins: schemes.pluginsAllResolved,
|
||||
});
|
||||
});
|
||||
@@ -53,14 +46,12 @@ describe('composeArtifactsConfig', () => {
|
||||
artifacts: {
|
||||
...schemes.allArtifactsConfiguration,
|
||||
rootDir: 'otherPlace',
|
||||
pathBuilder: _.noop,
|
||||
pathBuilder: '@global/pathbuilder',
|
||||
}
|
||||
},
|
||||
cliConfig: {},
|
||||
})).toMatchObject({
|
||||
pathBuilder: expect.objectContaining({
|
||||
rootDir: expect.stringMatching(/^otherPlace[\\/]abracadabra\.\d{4}/),
|
||||
}),
|
||||
rootDir: expect.stringMatching(/^otherPlace[\\/]abracadabra\.\d{4}/),
|
||||
plugins: schemes.pluginsAllResolved,
|
||||
});
|
||||
});
|
||||
@@ -75,14 +66,13 @@ describe('composeArtifactsConfig', () => {
|
||||
artifacts: {
|
||||
...schemes.allArtifactsConfiguration,
|
||||
rootDir: 'otherPlace',
|
||||
pathBuilder: _.noop,
|
||||
pathBuilder: '@global/pathbuilder',
|
||||
}
|
||||
},
|
||||
cliConfig: {},
|
||||
})).toMatchObject({
|
||||
pathBuilder: expect.objectContaining({
|
||||
rootDir: expect.stringMatching(/^artifacts[\\/]abracadabra\.\d{4}/),
|
||||
}),
|
||||
rootDir: expect.stringMatching(/^artifacts[\\/]abracadabra\.\d{4}/),
|
||||
pathBuilder: undefined,
|
||||
plugins: schemes.pluginsDefaultsResolved,
|
||||
});
|
||||
});
|
||||
@@ -96,15 +86,12 @@ describe('composeArtifactsConfig', () => {
|
||||
artifactsLocation: 'otherPlace',
|
||||
recordLogs: 'all',
|
||||
recordPerformance: 'all',
|
||||
recordTimeline: 'all',
|
||||
recordVideos: 'all',
|
||||
takeScreenshots: 'all',
|
||||
captureViewHierarchy: 'enabled',
|
||||
},
|
||||
})).toMatchObject({
|
||||
pathBuilder: expect.objectContaining({
|
||||
rootDir: expect.stringMatching(/^otherPlace[\\/]abracadabra\.\d{4}/),
|
||||
}),
|
||||
rootDir: expect.stringMatching(/^otherPlace[\\/]abracadabra\.\d{4}/),
|
||||
plugins: schemes.pluginsAllResolved,
|
||||
});
|
||||
});
|
||||
@@ -118,7 +105,7 @@ describe('composeArtifactsConfig', () => {
|
||||
localConfig: {
|
||||
artifacts: {
|
||||
rootDir: 'configuration',
|
||||
pathBuilder: _.identity,
|
||||
pathBuilder: '@local/pathbuilder',
|
||||
plugins: {
|
||||
log: 'failing',
|
||||
},
|
||||
@@ -127,58 +114,24 @@ describe('composeArtifactsConfig', () => {
|
||||
globalConfig: {
|
||||
artifacts: {
|
||||
rootDir: 'global',
|
||||
pathBuilder: _.noop,
|
||||
pathBuilder: '@global/pathbuilder',
|
||||
plugins: {
|
||||
screenshot: 'all',
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toMatchObject({
|
||||
pathBuilder: expect.objectContaining({
|
||||
rootDir: expect.stringMatching(/^cli[\\/]priority\.\d{4}/),
|
||||
}),
|
||||
rootDir: expect.stringMatching(/^cli[\\/]priority\.\d{4}/),
|
||||
pathBuilder: '@local/pathbuilder',
|
||||
plugins: {
|
||||
log: schemes.pluginsFailingResolved.log,
|
||||
screenshot: schemes.pluginsAllResolved.screenshot,
|
||||
video: schemes.pluginsDefaultsResolved.video,
|
||||
instruments: schemes.pluginsDefaultsResolved.instruments,
|
||||
timeline: schemes.pluginsDefaultsResolved.timeline,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve path builder from string (absolute path)', () => {
|
||||
const FakePathBuilder = require('../artifacts/__mocks__/FakePathBuilder');
|
||||
expect(composeArtifactsConfig({
|
||||
configurationName: 'customization',
|
||||
localConfig: {
|
||||
artifacts: {
|
||||
pathBuilder: path.join(__dirname, '../artifacts/__mocks__/FakePathBuilder')
|
||||
},
|
||||
},
|
||||
globalConfig: {},
|
||||
cliConfig: {},
|
||||
}).pathBuilder).toBeInstanceOf(FakePathBuilder);
|
||||
});
|
||||
|
||||
it('should resolve path builder from string (relative path)', () => {
|
||||
expect(composeArtifactsConfig({
|
||||
configurationName: 'customization',
|
||||
cliConfig: {},
|
||||
localConfig: {
|
||||
artifacts: {
|
||||
pathBuilder: './package.json',
|
||||
},
|
||||
},
|
||||
globalConfig: {},
|
||||
})).toMatchObject({
|
||||
pathBuilder: expect.objectContaining({
|
||||
'name': expect.any(String),
|
||||
'version': expect.any(String),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not append configuration with timestamp if rootDir ends with slash', () => {
|
||||
expect(composeArtifactsConfig({
|
||||
configurationName: 'customization',
|
||||
@@ -190,7 +143,7 @@ describe('composeArtifactsConfig', () => {
|
||||
globalConfig: {},
|
||||
cliConfig: {},
|
||||
})).toMatchObject({
|
||||
rootDir: '.artifacts/',
|
||||
rootDir: '.artifacts',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,7 +156,6 @@ describe('composeArtifactsConfig', () => {
|
||||
globalConfig: {
|
||||
artifacts: {
|
||||
rootDir: 'configuration',
|
||||
pathBuilder: _.identity,
|
||||
plugins: {
|
||||
screenshot: {
|
||||
takeWhen: {
|
||||
|
||||
@@ -15,6 +15,7 @@ function composeBehaviorConfig({
|
||||
.defaultsDeep(
|
||||
{
|
||||
init: {
|
||||
keepLockFile: cliConfig.keepLockFile ? true : undefined,
|
||||
reinstallApp: cliConfig.reuse ? false : undefined,
|
||||
},
|
||||
cleanup: {
|
||||
@@ -26,6 +27,7 @@ function composeBehaviorConfig({
|
||||
{
|
||||
init: {
|
||||
exposeGlobals: true,
|
||||
keepLockFile: false,
|
||||
reinstallApp: undefined,
|
||||
},
|
||||
launchApp: 'auto',
|
||||
|
||||
@@ -2,13 +2,12 @@ const _ = require('lodash');
|
||||
|
||||
describe('composeBehaviorConfig', () => {
|
||||
let composeBehaviorConfig;
|
||||
let cliConfig, localConfig, globalConfig, userParams;
|
||||
let cliConfig, localConfig, globalConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
cliConfig = {};
|
||||
localConfig = {};
|
||||
globalConfig = {};
|
||||
userParams = {};
|
||||
|
||||
composeBehaviorConfig = require('./composeBehaviorConfig');
|
||||
});
|
||||
@@ -17,12 +16,12 @@ describe('composeBehaviorConfig', () => {
|
||||
cliConfig,
|
||||
localConfig,
|
||||
globalConfig,
|
||||
userParams,
|
||||
});
|
||||
|
||||
it('should return a default behavior if nothing is set', () => {
|
||||
expect(composed()).toEqual({
|
||||
init: {
|
||||
keepLockFile: false,
|
||||
exposeGlobals: true,
|
||||
reinstallApp: true,
|
||||
},
|
||||
@@ -53,6 +52,7 @@ describe('composeBehaviorConfig', () => {
|
||||
behavior: {
|
||||
init: {
|
||||
exposeGlobals: false,
|
||||
keepLockFile: true,
|
||||
reinstallApp: false,
|
||||
},
|
||||
launchApp: 'manual',
|
||||
@@ -76,6 +76,7 @@ describe('composeBehaviorConfig', () => {
|
||||
behavior: {
|
||||
init: {
|
||||
exposeGlobals: true,
|
||||
keepLockFile: false,
|
||||
reinstallApp: true,
|
||||
},
|
||||
launchApp: 'auto',
|
||||
@@ -93,5 +94,27 @@ describe('composeBehaviorConfig', () => {
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if CLI config is set', () => {
|
||||
beforeEach(() => {
|
||||
cliConfig = {
|
||||
keepLockFile: true,
|
||||
reuse: true,
|
||||
cleanup: true,
|
||||
};
|
||||
});
|
||||
|
||||
it('should override the defaults from global config', () => {
|
||||
expect(composed()).toEqual(expect.objectContaining({
|
||||
init: expect.objectContaining({
|
||||
keepLockFile: true,
|
||||
reinstallApp: false,
|
||||
}),
|
||||
cleanup: expect.objectContaining({
|
||||
shutdownDevice: true,
|
||||
}),
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const environmentFactory = require('../environmentFactory');
|
||||
const log = require('../utils/logger').child({ __filename });
|
||||
const log = require('../utils/logger').child({ cat: 'config' });
|
||||
|
||||
/**
|
||||
* @param {DetoxConfigErrorComposer} opts.errorComposer
|
||||
@@ -118,7 +118,7 @@ function validateDeviceConfig({ deviceConfig, errorComposer, deviceAlias }) {
|
||||
throw errorComposer.malformedDeviceProperty(deviceAlias, 'gpuMode');
|
||||
}
|
||||
|
||||
if (!deviceConfig.gpuMode.match(/^(auto|host|swiftshader_indirect|angle_indirect|guest)$/)) {
|
||||
if (!deviceConfig.gpuMode.match(/^(auto|host|swiftshader_indirect|angle_indirect|guest|off)$/)) {
|
||||
throw errorComposer.malformedDeviceProperty(deviceAlias, 'gpuMode');
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('composeDeviceConfig', () => {
|
||||
'./customDriver'
|
||||
];
|
||||
|
||||
const KNOWN_GPU_MODES = ['auto', 'host', 'swiftshader_indirect', 'angle_indirect', 'guest'];
|
||||
const KNOWN_GPU_MODES = ['auto', 'host', 'swiftshader_indirect', 'angle_indirect', 'guest', 'off'];
|
||||
|
||||
/**
|
||||
* @param {'ios.simulator' | 'android.attached' | 'android.emulator' | 'android.genycloud' | './customDriver'} deviceType
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const { castLevel, defaultOptions } = require('../logger/DetoxLogger');
|
||||
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {Detox.DetoxConfig} opts.globalConfig
|
||||
* @param {Detox.DetoxConfiguration} opts.localConfig
|
||||
* @param {*} opts.cliConfig
|
||||
*/
|
||||
function composeLoggerConfig(opts) {
|
||||
const { globalConfig, localConfig, cliConfig } = opts;
|
||||
|
||||
const items = [
|
||||
{
|
||||
level: 'info',
|
||||
overrideConsole: 'sandbox',
|
||||
options: defaultOptions,
|
||||
},
|
||||
globalConfig.logger,
|
||||
localConfig.logger,
|
||||
adaptCLI(cliConfig),
|
||||
];
|
||||
|
||||
return items.reduce(
|
||||
/**
|
||||
* @param {Partial<Detox.DetoxLoggerConfig>} acc
|
||||
* @param config
|
||||
*/
|
||||
(acc, config) => {
|
||||
if (!config) return acc;
|
||||
const { options } = config;
|
||||
return _.merge(acc, {
|
||||
options: typeof options === 'function' ? options(acc) : options
|
||||
});
|
||||
},
|
||||
items.reduce((a, b) => _.merge(a, _.omit(b, 'options')))
|
||||
);
|
||||
}
|
||||
|
||||
function adaptCLI(cliConfig) {
|
||||
const result = {};
|
||||
|
||||
if (cliConfig.loglevel !== undefined) {
|
||||
result.level = castLevel(cliConfig.loglevel);
|
||||
}
|
||||
|
||||
if (cliConfig.useCustomLogger !== undefined) {
|
||||
result.overrideConsole = cliConfig.useCustomLogger ? 'all' : 'none';
|
||||
}
|
||||
|
||||
if (cliConfig.noColor) {
|
||||
result.options = { colors: false };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = composeLoggerConfig;
|
||||
@@ -0,0 +1,145 @@
|
||||
describe('composeLoggerConfig', () => {
|
||||
let composeLoggerConfig;
|
||||
let cliConfig, localConfig, globalConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
cliConfig = {};
|
||||
localConfig = {};
|
||||
globalConfig = {};
|
||||
|
||||
composeLoggerConfig = require('./composeLoggerConfig');
|
||||
});
|
||||
|
||||
let composed = () => composeLoggerConfig({
|
||||
cliConfig,
|
||||
localConfig,
|
||||
globalConfig,
|
||||
});
|
||||
|
||||
it('should return a default behavior if nothing is set', () => {
|
||||
expect(composed()).toEqual({
|
||||
level: 'info',
|
||||
overrideConsole: 'sandbox',
|
||||
options: {
|
||||
showDate: expect.any(Function),
|
||||
showLoggerName: true,
|
||||
showPid: true,
|
||||
showLevel: false,
|
||||
showMetadata: false,
|
||||
showPrefixes: expect.any(Function),
|
||||
basepath: expect.any(String),
|
||||
prefixers: {
|
||||
'ph': expect.any(Function),
|
||||
},
|
||||
stringifiers: {
|
||||
'args': expect.any(Function),
|
||||
'data': expect.any(Function),
|
||||
'error': expect.any(Function),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['local config'],
|
||||
['global config'],
|
||||
])('if a %s has overrides', (description) => {
|
||||
beforeEach(() => {
|
||||
const config = description.startsWith('local') ? localConfig : globalConfig;
|
||||
config.logger = {
|
||||
level: 'debug',
|
||||
overrideConsole: 'all',
|
||||
options: {
|
||||
showLoggerName: false,
|
||||
prefixers: {
|
||||
somethingElse: jest.fn(),
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should apply them upon the defaults', () => {
|
||||
expect(composed()).toEqual({
|
||||
level: 'debug',
|
||||
overrideConsole: 'all',
|
||||
options: expect.objectContaining({
|
||||
showLoggerName: false,
|
||||
showPrefixes: expect.any(Function),
|
||||
showPid: true,
|
||||
prefixers: expect.objectContaining({
|
||||
'cat': expect.any(Function),
|
||||
somethingElse: expect.any(Function),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if a CLI config has overrides', () => {
|
||||
beforeEach(() => {
|
||||
cliConfig.loglevel = 'trace';
|
||||
cliConfig.noColor = true;
|
||||
cliConfig.useCustomLogger = false;
|
||||
});
|
||||
|
||||
it('should apply them upon the defaults', () => {
|
||||
expect(composed()).toEqual({
|
||||
level: 'trace',
|
||||
overrideConsole: 'none',
|
||||
options: expect.objectContaining({
|
||||
colors: false,
|
||||
showPid: true,
|
||||
prefixers: expect.objectContaining({
|
||||
ph: expect.any(Function),
|
||||
id: expect.any(Function),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should adapt loglevel=verbose -> debug', () => {
|
||||
cliConfig.loglevel = 'verbose';
|
||||
|
||||
expect(composed()).toEqual(expect.objectContaining({
|
||||
level: 'debug',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
test('configs should have priority: CLI > local > global > defaults', () => {
|
||||
globalConfig.logger = {
|
||||
level: 'warn',
|
||||
overrideConsole: 'none',
|
||||
options: {
|
||||
colors: {},
|
||||
indent: '\t',
|
||||
showDate: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
localConfig.logger = {
|
||||
level: 'error',
|
||||
overrideConsole: 'sandbox',
|
||||
options: {
|
||||
colors: { 40: 'yellow' },
|
||||
showDate: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
cliConfig = {
|
||||
loglevel: 'fatal',
|
||||
noColor: true,
|
||||
useCustomLogger: true,
|
||||
};
|
||||
|
||||
expect(composed()).toEqual({
|
||||
level: 'fatal',
|
||||
overrideConsole: 'all',
|
||||
options: expect.objectContaining({
|
||||
colors: false,
|
||||
indent: '\t',
|
||||
showDate: localConfig.logger.options.showDate,
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,127 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* @param {Detox.DetoxConfig} globalConfig
|
||||
* @param {Detox.DetoxConfiguration} localConfig
|
||||
*/
|
||||
function composeRunnerConfig({ globalConfig, cliConfig }) {
|
||||
const testRunner = globalConfig.testRunner || 'jest';
|
||||
const customRunnerConfig = cliConfig.runnerConfig || globalConfig.runnerConfig;
|
||||
const os = require('os');
|
||||
|
||||
return {
|
||||
testRunner,
|
||||
runnerConfig: customRunnerConfig || 'e2e/config.json',
|
||||
specs: globalConfig.specs || '',
|
||||
};
|
||||
const _ = require('lodash');
|
||||
|
||||
const log = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {Detox.DetoxConfig} opts.globalConfig
|
||||
* @param {Detox.DetoxConfiguration} opts.localConfig
|
||||
* @param {DetoxInternals.CLIConfig} opts.cliConfig
|
||||
* @param {Record<string, any>} opts.testRunnerArgv
|
||||
* @param {import('../errors/DetoxConfigErrorComposer')} opts.errorComposer
|
||||
* @returns {Detox.DetoxTestRunnerConfig} opts.testRunnerArgv
|
||||
*/
|
||||
function composeRunnerConfig(opts) {
|
||||
const globalConfig = adaptLegacyRunnerConfig(opts.globalConfig);
|
||||
if (globalConfig != null && typeof globalConfig !== 'object') {
|
||||
throw opts.errorComposer.invalidTestRunnerProperty(true);
|
||||
}
|
||||
|
||||
const localConfig = opts.localConfig.testRunner;
|
||||
if (localConfig != null && typeof localConfig !== 'object') {
|
||||
throw opts.errorComposer.invalidTestRunnerProperty(false);
|
||||
}
|
||||
|
||||
const cliConfig = opts.cliConfig;
|
||||
|
||||
/** @type {Detox.DetoxTestRunnerConfig} */
|
||||
const merged = _.merge(
|
||||
{
|
||||
retries: 0,
|
||||
inspectBrk: inspectBrkHookDefault,
|
||||
jest: {
|
||||
initTimeout: 300000,
|
||||
retryAfterCircusRetries: false,
|
||||
reportSpecs: undefined,
|
||||
reportWorkerAssign: true,
|
||||
},
|
||||
args: {
|
||||
$0: 'jest',
|
||||
_: [],
|
||||
},
|
||||
},
|
||||
globalConfig,
|
||||
localConfig,
|
||||
cliConfig.retries != null ? { retries: cliConfig.retries } : null,
|
||||
cliConfig.jestReportSpecs != null ? { jest: { reportSpecs: cliConfig.jestReportSpecs } } : null,
|
||||
{
|
||||
args: _.omitBy(opts.testRunnerArgv, hasEmptyPositionalArgs)
|
||||
}
|
||||
);
|
||||
|
||||
if (typeof merged.inspectBrk === 'function') {
|
||||
if (cliConfig.inspectBrk) {
|
||||
merged.inspectBrk(merged);
|
||||
merged.inspectBrk = true;
|
||||
} else {
|
||||
merged.inspectBrk = false;
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function adaptLegacyRunnerConfig(globalConfig) {
|
||||
let isLegacy = false;
|
||||
|
||||
const runnerConfigKey = 'runnerConfig' in globalConfig ? 'runnerConfig' : 'runner-config';
|
||||
if (_.isString(globalConfig[runnerConfigKey])) {
|
||||
isLegacy = true;
|
||||
log.warn(`Detected a deprecated "${runnerConfigKey}" property (string).`);
|
||||
}
|
||||
|
||||
const testRunnerKey = 'testRunner' in globalConfig ? 'testRunner' : 'test-runner';
|
||||
if (_.isString(globalConfig[testRunnerKey])) {
|
||||
isLegacy = true;
|
||||
log.warn(`Detected a deprecated "${testRunnerKey}" property (string).`);
|
||||
}
|
||||
|
||||
if (globalConfig.specs != null) {
|
||||
isLegacy = true;
|
||||
log.warn(`Detected a deprecated "specs" property.`);
|
||||
}
|
||||
|
||||
if (!isLegacy) {
|
||||
return globalConfig.testRunner;
|
||||
}
|
||||
|
||||
log.warn(`Please migrate your Detox config according to the guide: [TODO: insert the migration guilde link]`);
|
||||
const testRunner = globalConfig[testRunnerKey];
|
||||
const runnerConfig = globalConfig[runnerConfigKey];
|
||||
const specs = globalConfig.specs != null ? String(globalConfig.specs) : undefined;
|
||||
|
||||
const args = {};
|
||||
if (_.isString(testRunner)) {
|
||||
args.$0 = testRunner;
|
||||
}
|
||||
|
||||
if (_.isString(runnerConfig)) {
|
||||
args.config = runnerConfig;
|
||||
}
|
||||
|
||||
if (specs) {
|
||||
args._ = [specs];
|
||||
}
|
||||
|
||||
return { args };
|
||||
}
|
||||
|
||||
function hasEmptyPositionalArgs(value, key) {
|
||||
return key === '_' ? _.isEmpty(value) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Detox.DetoxTestRunnerConfig} config
|
||||
*/
|
||||
function inspectBrkHookDefault(config) {
|
||||
config.args.$0 = /* istanbul ignore if */ os.platform() === 'win32'
|
||||
? `node --inspect-brk ./node_modules/jest/bin/jest.js`
|
||||
: `node --inspect-brk ./node_modules/.bin/jest`;
|
||||
config.args.runInBand = true;
|
||||
delete config.args.w;
|
||||
delete config.args.workers;
|
||||
}
|
||||
|
||||
module.exports = composeRunnerConfig;
|
||||
|
||||
@@ -1,53 +1,311 @@
|
||||
// @ts-nocheck
|
||||
describe('composeRunnerConfig', () => {
|
||||
let composeRunnerConfig;
|
||||
let cliConfig, globalConfig;
|
||||
let globalConfig, localConfig, cliConfig, testRunnerArgv;
|
||||
|
||||
beforeEach(() => {
|
||||
cliConfig = {};
|
||||
globalConfig = {};
|
||||
localConfig = {};
|
||||
cliConfig = {};
|
||||
testRunnerArgv = undefined;
|
||||
|
||||
composeRunnerConfig = () => require('./composeRunnerConfig')({
|
||||
cliConfig,
|
||||
globalConfig,
|
||||
composeRunnerConfig = () => {
|
||||
const { DetoxConfigErrorComposer } = require('../errors');
|
||||
const errorComposer = new DetoxConfigErrorComposer()
|
||||
.setConfigurationName('default')
|
||||
.setDetoxConfig({
|
||||
...globalConfig,
|
||||
configurations: {
|
||||
default: {
|
||||
...localConfig,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return require('./composeRunnerConfig')({
|
||||
globalConfig,
|
||||
localConfig,
|
||||
cliConfig,
|
||||
testRunnerArgv,
|
||||
errorComposer,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it('should return default test runner args as if it is Jest', () => {
|
||||
expect(composeRunnerConfig()).toEqual({
|
||||
args: {
|
||||
$0: 'jest',
|
||||
_: [],
|
||||
},
|
||||
jest: {
|
||||
initTimeout: 300000,
|
||||
retryAfterCircusRetries: false,
|
||||
reportSpecs: undefined,
|
||||
reportWorkerAssign: true,
|
||||
},
|
||||
retries: 0,
|
||||
inspectBrk: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should take .testRunner from globalConfig', () => {
|
||||
globalConfig.testRunner = 'nyc jest';
|
||||
expect(composeRunnerConfig().testRunner).toBe('nyc jest');
|
||||
it('should take overrides from globalConfig', () => {
|
||||
globalConfig.testRunner = {
|
||||
args: { $0: 'nyc jest' },
|
||||
jest: {
|
||||
initTimeout: 5000,
|
||||
retryAfterCircusRetries: true,
|
||||
reportSpecs: false,
|
||||
},
|
||||
retries: 1,
|
||||
inspectBrk: true,
|
||||
};
|
||||
|
||||
expect(composeRunnerConfig()).toEqual({
|
||||
args: {
|
||||
$0: 'nyc jest',
|
||||
_: [],
|
||||
},
|
||||
jest: {
|
||||
initTimeout: 5000,
|
||||
retryAfterCircusRetries: true,
|
||||
reportSpecs: false,
|
||||
reportWorkerAssign: true,
|
||||
},
|
||||
retries: 1,
|
||||
inspectBrk: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set .testRunner to "jest" by default', () => {
|
||||
expect(composeRunnerConfig().testRunner).toBe('jest');
|
||||
it('should take overrides from localConfig', () => {
|
||||
localConfig.testRunner = {
|
||||
args: { $0: 'nyc jest' },
|
||||
jest: {
|
||||
initTimeout: 120000,
|
||||
retryAfterCircusRetries: true,
|
||||
reportSpecs: true,
|
||||
},
|
||||
retries: 1,
|
||||
inspectBrk: true,
|
||||
};
|
||||
|
||||
expect(composeRunnerConfig()).toEqual({
|
||||
args: {
|
||||
$0: 'nyc jest',
|
||||
_: [],
|
||||
},
|
||||
jest: {
|
||||
initTimeout: 120000,
|
||||
retryAfterCircusRetries: true,
|
||||
reportSpecs: true,
|
||||
reportWorkerAssign: true,
|
||||
},
|
||||
retries: 1,
|
||||
inspectBrk: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should take .runnerConfig from CLI', () => {
|
||||
globalConfig.runnerConfig = 'from/config.json';
|
||||
cliConfig.runnerConfig = 'from/cli.json';
|
||||
it('should take --retries overrides from cliConfig', () => {
|
||||
localConfig.testRunner = {
|
||||
retries: 1,
|
||||
};
|
||||
|
||||
expect(composeRunnerConfig().runnerConfig).toBe('from/cli.json');
|
||||
cliConfig.retries = 5;
|
||||
|
||||
expect(composeRunnerConfig()).toEqual(expect.objectContaining({
|
||||
retries: 5,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should take .runnerConfig from config if it is not defined via CLI', () => {
|
||||
globalConfig.runnerConfig = 'from/config.json';
|
||||
delete cliConfig.runnerConfig;
|
||||
it('should take --inspect-brk overrides from cliConfig', () => {
|
||||
cliConfig.inspectBrk = true;
|
||||
|
||||
expect(composeRunnerConfig().runnerConfig).toBe('from/config.json');
|
||||
expect(composeRunnerConfig()).toEqual(expect.objectContaining({
|
||||
args: expect.objectContaining({
|
||||
$0: expect.stringMatching(/--inspect-brk.*jest/),
|
||||
runInBand: true,
|
||||
}),
|
||||
inspectBrk: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should set .runnerConfig to e2e/config.json by default', () => {
|
||||
globalConfig.testRunner = 'jest';
|
||||
expect(composeRunnerConfig().runnerConfig).toBe('e2e/config.json');
|
||||
it('should apply --jest-report-specs overrides from cliConfig onto globalConfig and localConfig', () => {
|
||||
globalConfig.testRunner = {
|
||||
jest: { customProperty: 1 },
|
||||
};
|
||||
|
||||
localConfig.testRunner = {
|
||||
jest: { otherProperty: true },
|
||||
};
|
||||
|
||||
cliConfig.jestReportSpecs = true;
|
||||
|
||||
expect(composeRunnerConfig()).toEqual(expect.objectContaining({
|
||||
jest: {
|
||||
customProperty: 1,
|
||||
initTimeout: 300000,
|
||||
otherProperty: true,
|
||||
retryAfterCircusRetries: false,
|
||||
reportSpecs: true,
|
||||
reportWorkerAssign: true,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it('should take .specs from globalConfig', () => {
|
||||
globalConfig.specs = 'e2e/suite1';
|
||||
expect(composeRunnerConfig().specs).toBe('e2e/suite1');
|
||||
it('should take overrides from testRunnerArgv', () => {
|
||||
testRunnerArgv = {
|
||||
listFiles: true,
|
||||
};
|
||||
|
||||
expect(composeRunnerConfig().args).toEqual({
|
||||
$0: 'jest',
|
||||
listFiles: true,
|
||||
_: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should treat undefined .specs as an empty string', () => {
|
||||
delete globalConfig.specs;
|
||||
expect(composeRunnerConfig().specs).toBe('');
|
||||
it('should prefer localConfig to globalConfig when overriding', () => {
|
||||
globalConfig.testRunner = {
|
||||
_privateGlobalProperty: false,
|
||||
args: {
|
||||
$0: 'nyc jest',
|
||||
_: ['first.test.js'],
|
||||
maxWorkers: 3,
|
||||
},
|
||||
jest: {
|
||||
reportSpecs: true,
|
||||
},
|
||||
retries: 1,
|
||||
};
|
||||
|
||||
localConfig.testRunner = {
|
||||
_privateLocalProperty: null,
|
||||
args: {
|
||||
$0: 'jest2',
|
||||
bail: true,
|
||||
_: ['second.test.js'],
|
||||
},
|
||||
jest: {
|
||||
reportSpecs: false,
|
||||
},
|
||||
retries: 3,
|
||||
};
|
||||
|
||||
expect(composeRunnerConfig()).toEqual({
|
||||
_privateGlobalProperty: false,
|
||||
_privateLocalProperty: null,
|
||||
args: {
|
||||
$0: 'jest2',
|
||||
bail: true,
|
||||
maxWorkers: 3,
|
||||
_: ['second.test.js'],
|
||||
},
|
||||
jest: {
|
||||
initTimeout: 300_000,
|
||||
retryAfterCircusRetries: false,
|
||||
reportSpecs: false,
|
||||
reportWorkerAssign: true,
|
||||
},
|
||||
retries: 3,
|
||||
inspectBrk: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should support positional CLI args override', () => {
|
||||
localConfig.testRunner = {
|
||||
args: {
|
||||
_: ['default.test.js'],
|
||||
verbose: true,
|
||||
},
|
||||
};
|
||||
|
||||
testRunnerArgv = {
|
||||
_: [],
|
||||
debug: true,
|
||||
};
|
||||
|
||||
expect(composeRunnerConfig().args).toEqual({
|
||||
$0: 'jest',
|
||||
debug: true,
|
||||
verbose: true,
|
||||
_: ['default.test.js'],
|
||||
});
|
||||
|
||||
testRunnerArgv._[0] = 'specific.test.js';
|
||||
expect(composeRunnerConfig().args._).toEqual(['specific.test.js']);
|
||||
});
|
||||
|
||||
it('should also support simple positional arguments merging with CLI args', () => {
|
||||
localConfig.testRunner = {
|
||||
args: {
|
||||
_: ['fallback.test.js']
|
||||
},
|
||||
};
|
||||
|
||||
testRunnerArgv = { _: [] };
|
||||
expect(composeRunnerConfig().args._).toEqual(['fallback.test.js']);
|
||||
|
||||
testRunnerArgv = { _: undefined };
|
||||
expect(composeRunnerConfig().args._).toEqual(['fallback.test.js']);
|
||||
|
||||
testRunnerArgv = { _: ['override.test.js'] };
|
||||
expect(composeRunnerConfig().args._).toEqual(['override.test.js']);
|
||||
});
|
||||
|
||||
describe('legacy fallbacks', () => {
|
||||
let log;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mock('../utils/logger');
|
||||
log = require('../utils/logger');
|
||||
});
|
||||
|
||||
test('deprecated "test-runner"', () => {
|
||||
globalConfig['test-runner'] = 'nyc jest';
|
||||
expect(composeRunnerConfig().args).toEqual({ $0: 'nyc jest', _: [] });
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('"test-runner" property'));
|
||||
});
|
||||
|
||||
test('deprecated "testRunner"', () => {
|
||||
globalConfig['testRunner'] = 'nyc jest';
|
||||
expect(composeRunnerConfig().args).toEqual({ $0: 'nyc jest', _: [] });
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('"testRunner" property'));
|
||||
});
|
||||
|
||||
test('deprecated "runner-config"', () => {
|
||||
globalConfig['runner-config'] = 'e2e/config.json';
|
||||
expect(composeRunnerConfig().args).toEqual({ $0: 'jest', config: 'e2e/config.json', _: [] });
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('"runner-config" property'));
|
||||
});
|
||||
|
||||
test('deprecated "runnerConfig"', () => {
|
||||
globalConfig['runnerConfig'] = 'e2e/config.json';
|
||||
expect(composeRunnerConfig().args).toEqual({ $0: 'jest', config: 'e2e/config.json', _: [] });
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('"runnerConfig" property'));
|
||||
});
|
||||
|
||||
test('deprecated "specs"', () => {
|
||||
globalConfig['specs'] = '.';
|
||||
expect(composeRunnerConfig().args).toEqual({ $0: 'jest', _: ['.'] });
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('"specs" property'));
|
||||
});
|
||||
|
||||
test('deprecated "specs": empty array workaround', () => {
|
||||
globalConfig['specs'] = [];
|
||||
expect(composeRunnerConfig().args).toEqual({ $0: 'jest', _: [] });
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('"specs" property'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('unhappy scenarios', () => {
|
||||
test('non-string and non-object "testRunner" in global config', () => {
|
||||
globalConfig['testRunner'] = 0;
|
||||
expect(composeRunnerConfig).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('deprecated "testRunner" in local config', () => {
|
||||
localConfig['testRunner'] = 'nyc jest';
|
||||
expect(composeRunnerConfig).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const isValidWebsocketURL = require('../utils/isValidWebsocketURL');
|
||||
const uuid = require('../utils/uuid');
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
@@ -44,7 +43,6 @@ async function composeSessionConfig(options) {
|
||||
|
||||
const result = {
|
||||
autoStart: !session.server,
|
||||
sessionId: uuid.UUID(),
|
||||
debugSynchronization: 10000,
|
||||
|
||||
...session,
|
||||
|
||||
@@ -26,19 +26,11 @@ describe('composeSessionConfig', () => {
|
||||
autoStart: true,
|
||||
debugSynchronization: 10000,
|
||||
server: undefined,
|
||||
sessionId: expect.any(String),
|
||||
sessionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
describe('sessionId', function() {
|
||||
describe('by default', () => {
|
||||
it('should be autogenerated GUID', async () => {
|
||||
expect(await compose()).toMatchObject({
|
||||
sessionId: expect.stringMatching(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass validations', async () => {
|
||||
globalConfig.session = { sessionId: 1234 };
|
||||
await expect(compose()).rejects.toThrowError(errorComposer.invalidSessionIdProperty());
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const InstrumentsArtifactPlugin = require('../artifacts/instruments/InstrumentsArtifactPlugin');
|
||||
const LogArtifactPlugin = require('../artifacts/log/LogArtifactPlugin');
|
||||
const ScreenshotArtifactPlugin = require('../artifacts/screenshot/ScreenshotArtifactPlugin');
|
||||
const TimelineArtifactPlugin = require('../artifacts/timeline/TimelineArtifactPlugin');
|
||||
const IosUIHierarchyPlugin = require('../artifacts/uiHierarchy/IosUIHierarchyPlugin');
|
||||
const VideoArtifactPlugin = require('../artifacts/video/VideoArtifactPlugin');
|
||||
|
||||
@@ -13,7 +12,6 @@ const defaultArtifactsConfiguration = {
|
||||
screenshot: 'manual',
|
||||
video: 'none',
|
||||
instruments: 'none',
|
||||
timeline: 'none',
|
||||
uiHierarchy: 'disabled',
|
||||
},
|
||||
};
|
||||
@@ -26,7 +24,6 @@ const allArtifactsConfiguration = {
|
||||
screenshot: 'all',
|
||||
video: 'all',
|
||||
instruments: 'all',
|
||||
timeline: 'all',
|
||||
uiHierarchy: 'enabled',
|
||||
},
|
||||
};
|
||||
@@ -36,7 +33,6 @@ const pluginsDefaultsResolved = {
|
||||
screenshot: ScreenshotArtifactPlugin.parseConfig('manual'),
|
||||
video: VideoArtifactPlugin.parseConfig('none'),
|
||||
instruments: InstrumentsArtifactPlugin.parseConfig('none'),
|
||||
timeline: TimelineArtifactPlugin.parseConfig('none'),
|
||||
uiHierarchy: IosUIHierarchyPlugin.parseConfig('disabled'),
|
||||
};
|
||||
|
||||
@@ -51,7 +47,6 @@ const pluginsAllResolved = {
|
||||
screenshot: ScreenshotArtifactPlugin.parseConfig('all'),
|
||||
video: VideoArtifactPlugin.parseConfig('all'),
|
||||
instruments: InstrumentsArtifactPlugin.parseConfig('all'),
|
||||
timeline: TimelineArtifactPlugin.parseConfig('all'),
|
||||
uiHierarchy: IosUIHierarchyPlugin.parseConfig('enabled'),
|
||||
};
|
||||
|
||||
|
||||
@@ -8,20 +8,18 @@ const composeAppsConfig = require('./composeAppsConfig');
|
||||
const composeArtifactsConfig = require('./composeArtifactsConfig');
|
||||
const composeBehaviorConfig = require('./composeBehaviorConfig');
|
||||
const composeDeviceConfig = require('./composeDeviceConfig');
|
||||
const composeLoggerConfig = require('./composeLoggerConfig');
|
||||
const composeRunnerConfig = require('./composeRunnerConfig');
|
||||
const composeSessionConfig = require('./composeSessionConfig');
|
||||
const loadExternalConfig = require('./loadExternalConfig');
|
||||
const selectConfiguration = require('./selectConfiguration');
|
||||
|
||||
const hooks = {
|
||||
UNSAFE_configReady: [],
|
||||
};
|
||||
|
||||
async function composeDetoxConfig({
|
||||
cwd = process.cwd(),
|
||||
argv = undefined,
|
||||
testRunnerArgv = undefined,
|
||||
errorComposer = new DetoxConfigErrorComposer(),
|
||||
override,
|
||||
override = undefined,
|
||||
}) {
|
||||
const cliConfig = collectCliConfig({ argv });
|
||||
const findupResult = await loadExternalConfig({
|
||||
@@ -44,11 +42,6 @@ async function composeDetoxConfig({
|
||||
|
||||
const { configurations } = globalConfig;
|
||||
|
||||
const runnerConfig = composeRunnerConfig({
|
||||
globalConfig,
|
||||
cliConfig,
|
||||
});
|
||||
|
||||
const configurationName = selectConfiguration({
|
||||
errorComposer,
|
||||
globalConfig,
|
||||
@@ -61,6 +54,14 @@ async function composeDetoxConfig({
|
||||
throw errorComposer.configurationShouldNotUseLegacyFormat();
|
||||
}
|
||||
|
||||
const runnerConfig = composeRunnerConfig({
|
||||
globalConfig,
|
||||
localConfig,
|
||||
cliConfig,
|
||||
testRunnerArgv,
|
||||
errorComposer,
|
||||
});
|
||||
|
||||
const deviceConfig = composeDeviceConfig({
|
||||
errorComposer,
|
||||
globalConfig,
|
||||
@@ -90,6 +91,12 @@ async function composeDetoxConfig({
|
||||
cliConfig,
|
||||
});
|
||||
|
||||
const loggerConfig = composeLoggerConfig({
|
||||
globalConfig,
|
||||
localConfig,
|
||||
cliConfig,
|
||||
});
|
||||
|
||||
const sessionConfig = await composeSessionConfig({
|
||||
errorComposer,
|
||||
globalConfig,
|
||||
@@ -98,29 +105,26 @@ async function composeDetoxConfig({
|
||||
});
|
||||
|
||||
const result = {
|
||||
appsConfig,
|
||||
artifactsConfig,
|
||||
behaviorConfig,
|
||||
cliConfig,
|
||||
configurationName,
|
||||
deviceConfig,
|
||||
errorComposer,
|
||||
runnerConfig,
|
||||
sessionConfig,
|
||||
|
||||
apps: appsConfig,
|
||||
artifacts: artifactsConfig,
|
||||
behavior: behaviorConfig,
|
||||
cli: cliConfig,
|
||||
device: deviceConfig,
|
||||
logger: loggerConfig,
|
||||
testRunner: runnerConfig,
|
||||
session: sessionConfig,
|
||||
};
|
||||
|
||||
for (const fn of hooks.UNSAFE_configReady) {
|
||||
await fn({ ...result, argv });
|
||||
}
|
||||
Object.defineProperty(result, 'errorComposer', {
|
||||
enumerable: false,
|
||||
value: errorComposer,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function hook(event, listener) {
|
||||
hooks[event].push(listener);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
composeDetoxConfig,
|
||||
hook,
|
||||
};
|
||||
|
||||
@@ -64,15 +64,13 @@ describe('composeDetoxConfig', () => {
|
||||
cleanup: true,
|
||||
reuse: true,
|
||||
'record-logs': 'all',
|
||||
'runner-config': 'e2e/jest.config.js',
|
||||
},
|
||||
testRunnerArgv: {
|
||||
config: 'e2e/jest.config.js',
|
||||
},
|
||||
override: {
|
||||
artifacts: {
|
||||
pathBuilder: class {
|
||||
constructor() {
|
||||
this.testProperty = 42;
|
||||
}
|
||||
},
|
||||
pathBuilder: '@some/pathbuilder-implementation',
|
||||
plugins: {
|
||||
log: 'none',
|
||||
video: 'failing',
|
||||
@@ -98,10 +96,8 @@ describe('composeDetoxConfig', () => {
|
||||
configurationName: 'another',
|
||||
filepath: path.join(__dirname, '__mocks__/configuration/packagejson/package.json'),
|
||||
},
|
||||
artifactsConfig: {
|
||||
pathBuilder: {
|
||||
testProperty: 42,
|
||||
},
|
||||
artifacts: {
|
||||
pathBuilder: '@some/pathbuilder-implementation',
|
||||
plugins: {
|
||||
log: {
|
||||
enabled: true,
|
||||
@@ -113,7 +109,7 @@ describe('composeDetoxConfig', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
behaviorConfig: {
|
||||
behavior: {
|
||||
init: {
|
||||
exposeGlobals: true,
|
||||
reinstallApp: false,
|
||||
@@ -122,52 +118,36 @@ describe('composeDetoxConfig', () => {
|
||||
shutdownDevice: true,
|
||||
}
|
||||
},
|
||||
cliConfig: {
|
||||
cli: {
|
||||
configuration: 'another',
|
||||
deviceName: 'iPhone XS',
|
||||
cleanup: true,
|
||||
reuse: true,
|
||||
recordLogs: 'all',
|
||||
runnerConfig: 'e2e/jest.config.js',
|
||||
},
|
||||
deviceConfig: expect.objectContaining({
|
||||
device: expect.objectContaining({
|
||||
type: 'ios.simulator',
|
||||
device: {
|
||||
type: 'iPhone XS',
|
||||
},
|
||||
}),
|
||||
runnerConfig: {
|
||||
runnerConfig: 'e2e/jest.config.js',
|
||||
logger: {
|
||||
level: 'info',
|
||||
overrideConsole: 'sandbox',
|
||||
options: expect.objectContaining({}),
|
||||
},
|
||||
sessionConfig: expect.objectContaining({
|
||||
testRunner: {
|
||||
args: {
|
||||
$0: 'jest',
|
||||
config: 'e2e/jest.config.js',
|
||||
_: [],
|
||||
},
|
||||
},
|
||||
session: expect.objectContaining({
|
||||
server: 'ws://localhost:9999',
|
||||
sessionId: 'external file works',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should enable to add hooks on UNSAFE_configReady', async () => {
|
||||
const listener = jest.fn();
|
||||
configuration.hook('UNSAFE_configReady', listener);
|
||||
|
||||
await configuration.composeDetoxConfig({
|
||||
cwd: path.join(__dirname, '__mocks__/configuration/packagejson'),
|
||||
override: {
|
||||
configurations: {
|
||||
simple: {
|
||||
binaryPath: 'path/to/app',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
||||
appsConfig: expect.any(Object),
|
||||
artifactsConfig: expect.any(Object),
|
||||
behaviorConfig: expect.any(Object),
|
||||
cliConfig: expect.any(Object),
|
||||
deviceConfig: expect.any(Object),
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ const fs = require('fs-extra');
|
||||
const _ = require('lodash');
|
||||
const resolveFrom = require('resolve-from');
|
||||
|
||||
const log = require('../utils/logger').child({ __filename });
|
||||
const log = require('../utils/logger').child({ cat: 'config' });
|
||||
|
||||
async function locateExternalConfig(cwd) {
|
||||
return findUp([
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-nocheck
|
||||
const { traceCall } = require('../../utils/trace');
|
||||
const log = require('../../utils/logger').child({ cat: 'device' });
|
||||
const traceMethods = require('../../utils/traceMethods');
|
||||
|
||||
class DeviceAllocator {
|
||||
/**
|
||||
@@ -7,6 +8,7 @@ class DeviceAllocator {
|
||||
*/
|
||||
constructor(allocationDriver) {
|
||||
this._driver = allocationDriver;
|
||||
traceMethods(log, this, ['allocate', 'free']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -14,8 +16,7 @@ class DeviceAllocator {
|
||||
* @return {Promise<DeviceCookie>}
|
||||
*/
|
||||
allocate(deviceConfig) {
|
||||
return traceCall('allocateDevice', () =>
|
||||
this._driver.allocate(deviceConfig));
|
||||
return this._driver.allocate(deviceConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ const _ = require('lodash');
|
||||
|
||||
const DetoxRuntimeError = require('../../../../../errors/DetoxRuntimeError');
|
||||
const environment = require('../../../../../utils/environment');
|
||||
const logger = require('../../../../../utils/logger').child({ __filename });
|
||||
const logger = require('../../../../../utils/logger').child({ cat: 'device' });
|
||||
|
||||
const REQUIRED_EMULATOR_MAJOR = 29;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user