Skip to main content
Version: Next

Artifacts System

The artifacts subsystem collects test artifacts like screenshots, videos, logs, and performance data during test execution.

Architecture Overview​

┌─────────────────────────────────────────────────────────────┐
│ Artifacts Subsystem │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ ArtifactsManager │ │
│ │ │ │
│ │ - Registers plugins │ │
│ │ - Subscribes to device events │ │
│ │ - Orchestrates lifecycle callbacks │ │
│ │ - Manages idle callback queue │ │
│ └────────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ │ │ │ │
│ ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ │
│ │Screenshot │ │ Video │ │ Log │ │
│ │ Plugin │ │ Plugin │ │ Plugin │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ ┌───────────┐ ┌───────────┐ │
│ │Instruments│ │UIHierarchy│ │
│ │ Plugin │ │ (iOS) │ │
│ └───────────┘ └───────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ ArtifactPathBuilder │ │
│ │ Generates paths: artifacts/{config}/{test}/artifact │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Directory Structure​

src/artifacts/
├── ArtifactsManager.js # Main orchestrator
├── providers/
│ └── index.js # Exports: AndroidArtifactPluginsProvider,
│ # IosArtifactPluginsProvider,
│ # IosSimulatorArtifactPluginsProvider
├── templates/
│ ├── artifact/
│ │ ├── Artifact.js # Base artifact class
│ │ └── FileArtifact.js # File-based artifact
│ └── plugin/
│ ├── ArtifactPlugin.js # Base plugin class
│ ├── StartupAndTestRecorderPlugin.js
│ ├── TwoSnapshotsPerTestPlugin.js
│ └── WholeTestRecorderPlugin.js
├── screenshot/
│ ├── ScreenshotArtifactPlugin.js
│ ├── SimulatorScreenshotPlugin.js # iOS
│ └── ADBScreencapPlugin.js # Android
├── video/
│ ├── VideoArtifactPlugin.js
│ ├── SimulatorRecordVideoPlugin.js # iOS
│ └── ADBScreenrecorderPlugin.js # Android
├── log/
│ ├── LogArtifactPlugin.js
│ ├── ios/
│ │ └── SimulatorLogPlugin.js
│ └── android/
│ └── ADBLogcatPlugin.js
├── instruments/
│ ├── InstrumentsArtifactPlugin.js
│ ├── ios/
│ │ └── SimulatorInstrumentsPlugin.js
│ └── android/
│ └── AndroidInstrumentsPlugin.js
├── uiHierarchy/
│ └── IosUIHierarchyPlugin.js # iOS only
└── utils/
├── ArtifactPathBuilder.js
└── temporaryPath.js

ArtifactsManager​

Initialization​

class ArtifactsManager extends EventEmitter {
constructor({ rootDir, pathBuilder, plugins }) {
this._pluginConfigs = plugins;
this._artifactPlugins = {};
this._pathBuilder = new ArtifactPathBuilder({ rootDir });
this._idlePromise = Promise.resolve();
}

registerArtifactPlugins(artifactPluginFactoriesMap) {
for (const [key, factory] of Object.entries(artifactPluginFactoriesMap)) {
const config = this._pluginConfigs[key];
this._artifactPlugins[key] = this._instantiateArtifactPlugin(factory, config);
}
}
}

Plugin Instantiation​

Each plugin receives an API object for interacting with the manager:

_instantiateArtifactPlugin(pluginFactory, pluginUserConfig) {
const artifactsApi = {
plugin: null,
userConfig: { ...pluginUserConfig },

preparePathForArtifact: async (artifactName, testSummary) => {
const artifactPath = this._pathBuilder.buildPathForTestArtifact(artifactName, testSummary);
const artifactDir = path.dirname(artifactPath);
await fs.ensureDir(artifactDir);
return artifactPath;
},

trackArtifact: (artifact) => this.emit('trackArtifact', artifact),
untrackArtifact: (artifact) => this.emit('untrackArtifact', artifact),

requestIdleCallback: (callback) => {
// Queue for execution during idle time
this._idleCallbackRequests.push({ caller: artifactsApi.plugin, callback });
return this._idlePromise;
},
};

const plugin = pluginFactory(artifactsApi);
artifactsApi.plugin = plugin;
return plugin;
}

Device Event Subscription​

subscribeToDeviceEvents(deviceEmitter) {
deviceEmitter.on('bootDevice', this.onBootDevice.bind(this));
deviceEmitter.on('beforeShutdownDevice', this.onBeforeShutdownDevice.bind(this));
deviceEmitter.on('shutdownDevice', this.onShutdownDevice.bind(this));
deviceEmitter.on('beforeLaunchApp', this.onBeforeLaunchApp.bind(this));
deviceEmitter.on('launchApp', this.onLaunchApp.bind(this));
deviceEmitter.on('appReady', this.onAppReady.bind(this));
deviceEmitter.on('beforeUninstallApp', this.onBeforeUninstallApp.bind(this));
deviceEmitter.on('beforeTerminateApp', this.onBeforeTerminateApp.bind(this));
deviceEmitter.on('terminateApp', this.onTerminateApp.bind(this));
deviceEmitter.on('createExternalArtifact', this.onCreateExternalArtifact.bind(this));
}

Lifecycle Events​

// Test lifecycle
async onRunDescribeStart(suite) {
await this._callPlugins('ascending', 'onRunDescribeStart', suite);
}

async onTestStart(testSummary) {
await this._callPlugins('ascending', 'onTestStart', testSummary);
}

async onTestDone(testSummary) {
await this._callPlugins('descending', 'onTestDone', testSummary);
}

async onRunDescribeFinish(suite) {
await this._callPlugins('descending', 'onRunDescribeFinish', suite);
}

// Failure hooks
async onHookFailure(testSummary) {
await this._callPlugins('plain', 'onHookFailure', testSummary);
}

async onTestFnFailure(testSummary) {
await this._callPlugins('plain', 'onTestFnFailure', testSummary);
}

Plugin Execution Strategy​

Plugins can be called in different orders based on priority:

async _callPlugins(strategy, methodName, ...args) {
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 });
}
}));
}
}

_groupPlugins(strategy) {
if (strategy === 'plain') {
return [_.values(this._artifactPlugins)];
}

const byPriority = _.chain(this._artifactPlugins)
.values()
.groupBy('priority')
.entries()
.sortBy(([priority]) => Number(priority))
.map(1)
.value();

return strategy === 'descending' ? byPriority.reverse() : byPriority;
}

Plugin Types​

Base Plugin (src/artifacts/templates/plugin/ArtifactPlugin.js)​

class ArtifactPlugin {
constructor({ api }) {
this.api = api;
this.enabled = api.userConfig.enabled !== false;
this.keepOnlyFailedTestsArtifacts = api.userConfig.keepOnlyFailedTestsArtifacts;
}

// Lifecycle hooks (override in subclasses)
async onBootDevice(deviceInfo) {}
async onBeforeLaunchApp(appLaunchInfo) {}
async onLaunchApp(appLaunchInfo) {}
async onTestStart(testSummary) {}
async onTestDone(testSummary) {}
async onBeforeCleanup() {}
}

Screenshot Plugin​

Captures screenshots on test failure or on-demand:

class ScreenshotArtifactPlugin extends TwoSnapshotsPerTestPlugin {
async preparePathForSnapshot(testSummary, name) {
return this.api.preparePathForArtifact(`${name}.png`, testSummary);
}

async takeSnapshot(name) {
// Delegate to device driver
return this.driver.takeScreenshot(name);
}
}

Configuration:

{
artifacts: {
plugins: {
screenshot: {
enabled: true,
shouldTakeAutomaticSnapshots: true,
keepOnlyFailedTestsArtifacts: true,
}
}
}
}

Video Plugin​

Records test execution:

class VideoArtifactPlugin extends WholeTestRecorderPlugin {
async onTestStart(testSummary) {
await this.startRecording();
}

async onTestDone(testSummary) {
await this.stopRecording();

if (testSummary.status === 'passed' && this.keepOnlyFailedTestsArtifacts) {
await this.discardRecording();
} else {
await this.saveRecording(testSummary);
}
}
}

Log Plugin​

Aggregates device and app logs:

class LogArtifactPlugin extends StartupAndTestRecorderPlugin {
async onLaunchApp(appLaunchInfo) {
await this.startCapturingLogs(appLaunchInfo);
}

async onTestDone(testSummary) {
const logs = await this.stopCapturingLogs();
await this.saveLogs(logs, testSummary);
}
}

Instruments Plugin (iOS)​

Records performance profiles:

class InstrumentsArtifactPlugin extends ArtifactPlugin {
async startRecording({ recordingPath, samplingInterval }) {
await this.client.startInstrumentsRecording({ recordingPath, samplingInterval });
}

async stopRecording() {
await this.client.stopInstrumentsRecording();
}
}

UI Hierarchy Plugin (iOS)​

Captures view hierarchy for debugging (iOS only via IosUIHierarchyPlugin):

class IosUIHierarchyPlugin extends ArtifactPlugin {
async onTestFnFailure(testSummary) {
const hierarchy = await this.client.captureViewHierarchy();
await this.saveHierarchy(hierarchy, testSummary);
}
}

Platform-Specific Implementations​

Each artifact type has platform-specific implementations:

ArtifactiOS (Simulator)Android
ScreenshotSimulatorScreenshotPluginADBScreencapPlugin
VideoSimulatorRecordVideoPluginADBScreenrecorderPlugin
LogSimulatorLogPluginADBLogcatPlugin
InstrumentsSimulatorInstrumentsPluginAndroidInstrumentsPlugin
UI HierarchyIosUIHierarchyPluginNot available

The appropriate plugin is selected by the provider based on the device type:

  • IosSimulatorArtifactPluginsProvider for iOS simulators
  • AndroidArtifactPluginsProvider for Android devices

Artifact Templates​

Two Snapshots Per Test​

Takes snapshots at test start and end:

class TwoSnapshotsPerTestPlugin extends ArtifactPlugin {
async onTestStart(testSummary) {
if (this.shouldTakeAutomaticSnapshots) {
await this.takeSnapshot('testStart');
}
}

async onTestDone(testSummary) {
if (this.shouldTakeAutomaticSnapshots) {
await this.takeSnapshot('testDone');
}
}
}

Whole Test Recorder​

Records throughout entire test:

class WholeTestRecorderPlugin extends ArtifactPlugin {
async onTestStart(testSummary) {
await this.startRecording(testSummary);
}

async onTestDone(testSummary) {
await this.stopRecording();
await this.saveOrDiscard(testSummary);
}
}

Path Building​

ArtifactPathBuilder​

Generates consistent artifact paths:

class ArtifactPathBuilder {
constructor({ rootDir }) {
this._rootDir = rootDir;
}

buildPathForTestArtifact(artifactName, testSummary) {
// Path format: rootDir/{statusSign}{fullName}({invocations})/{artifactName}
// Example: artifacts/✓ Login should work/screenshot.png
// Example: artifacts/✗ Login should work (2)/screenshot.png (on retry)
const testDir = this._constructDirectoryNameForCurrentRunningTest(testSummary);
return path.join(this._rootDir, testDir, artifactName);
}

_constructDirectoryNameForCurrentRunningTest(testSummary) {
const prefix = this._buildTestDirectoryPrefix(testSummary);
const suffix = testSummary.invocations > 1 ? ` (${testSummary.invocations})` : '';
return constructSafeFilename(prefix, testSummary.fullName, suffix);
}

_buildTestDirectoryPrefix(testSummary) {
return testSummary.status === 'passed' ? '✓ ' : '✗ ';
}
}

Custom path builders can be provided:

// detox.config.js
module.exports = {
artifacts: {
rootDir: 'e2e-artifacts',
pathBuilder: './my-path-builder.js',
}
};

Configuration​

module.exports = {
artifacts: {
rootDir: 'artifacts',
pathBuilder: undefined, // or custom path builder
plugins: {
screenshot: {
enabled: true,
shouldTakeAutomaticSnapshots: true,
keepOnlyFailedTestsArtifacts: true,
},
video: {
enabled: true,
keepOnlyFailedTestsArtifacts: true,
},
log: {
enabled: true,
keepOnlyFailedTestsArtifacts: false,
},
instruments: {
enabled: false,
},
uiHierarchy: { // iOS only
enabled: true,
keepOnlyFailedTestsArtifacts: true,
},
},
},
};

Lifecycle Flow​

Test Suite Starts
│
▼
onRunDescribeStart(suite)
│
▼
┌────────────────────────────────┐
│ For each test: │
│ │
│ onTestStart(testSummary) │
│ │ │
│ ▼ │
│ [Test executes] │
│ │ │
│ ┌────┴────┐ │
│ │ Failure │ │
│ └────┬────┘ │
│ │ │
│ onHookFailure() or │
│ onTestFnFailure() │
│ │ │
│ ▼ │
│ onTestDone(testSummary) │
│ │ │
│ [Save or discard artifacts] │
│ │
└────────────────────────────────┘
│
▼
onRunDescribeFinish(suite)
│
▼
onBeforeCleanup()
│
▼
[Final artifact processing]

Idle Callbacks​

Plugins can defer non-critical work:

// In plugin
async onTestDone(testSummary) {
// Critical work first
await this.stopRecording();

// Defer compression to idle time
this.api.requestIdleCallback(async () => {
await this.compressVideo();
await this.uploadToCloud();
});
}

External Artifacts​

Plugins can create artifacts from external sources:

// During test
device.createExternalArtifact({
pluginId: 'screenshot',
artifactName: 'custom-screenshot',
artifactPath: '/tmp/screenshot.png',
});

// In ArtifactsManager
async onCreateExternalArtifact({ pluginId, artifactName, artifactPath }) {
await this._callSinglePlugin(pluginId, 'onCreateExternalArtifact', {
artifact: new FileArtifact({ temporaryPath: artifactPath }),
name: artifactName,
});
}

See Also​