Skip to content

Commit f1f5b32

Browse files
authored
feat: video recordings (#24)
1 parent 302f075 commit f1f5b32

File tree

13 files changed

+285
-21
lines changed

13 files changed

+285
-21
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2023 Wix Incubator
3+
Copyright (c) 2025 Wix Incubator
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ module.exports = {
4949
useSteps: true,
5050
deviceLogs: true,
5151
deviceScreenshots: true,
52+
deviceVideos: true,
5253
}],
5354
],
5455
},
@@ -62,6 +63,89 @@ Here's a brief explanation of what you just added:
6263

6364
- `testEnvironmentOptions` section: We added three event listener modules that will run during our tests — `jest-metadata`, `jest-allure2-reporter`, and `detox-allure2-adapter`. These listeners will collect necessary metadata and feed test result data to our Allure reports.
6465

66+
## Adapter Options
67+
68+
### `useSteps: boolean`
69+
70+
If set to true, the adapter will wrap all Detox device interactions (like `device.launchApp()`, `element(by.id('loginButton')).tap()`) into Allure steps. This provides a detailed, step-by-step report of your test execution.
71+
72+
### `deviceLogs: boolean | DetoxAllure2AdapterDeviceLogsOptions`
73+
74+
Enables capturing device (iOS/Android) logs for each step. This feature uses the [`logkitten`](https://www.npmjs.com/package/logkitten) library.
75+
76+
**Configuration:**
77+
78+
- **`true`**: Enables log capture with default settings.
79+
- **`false`** (default): Disables log capture.
80+
- **`DetoxAllure2AdapterDeviceLogsOptions`** (object): Enables log capture and provides fine-grained control over the settings.
81+
82+
- `ios: (entry: IosEntry) => boolean`: Filter function for iOS logs. Return `true` to include the log entry.
83+
- `android: (entry: AndroidEntry) => boolean`: Filter function for Android logs. Return `true` to include the log entry.
84+
- `override: boolean`: Whether to override existing log handlers.
85+
- `saveAll: boolean` (default: `false`): If `true`, saves logs for all steps. By default, only logs for failed steps are kept.
86+
- `syncDelay: number | { ios?: number; android?: number }` (default: `500`): Synchronization delay in milliseconds for log collection. Set to `0` to disable, or provide per-platform delays.
87+
88+
### `deviceScreenshots: boolean | DetoxAllure2AdapterDeviceScreenshotOptions`
89+
90+
Enables taking screenshots for each step. This feature uses the [`screenkitten`](https://www.npmjs.com/package/screenkitten) library.
91+
92+
**Configuration:**
93+
94+
- **`true`**: Enables screenshot capture with default settings.
95+
- **`false`** (default): Disables screenshot capture.
96+
- **`DetoxAllure2AdapterDeviceScreenshotOptions`** (object): Enables screenshot capture and provides fine-grained control over the settings.
97+
98+
- `saveAll: boolean` (default: `false`): If `true`, saves screenshots for all steps. By default, only screenshots for failed steps are kept.
99+
100+
### `deviceVideos: boolean | DetoxAllure2AdapterDeviceVideoOptions`
101+
102+
Enables "on-demand" video recording for your tests. This feature uses the [`videokitten`](https://www.npmjs.com/package/videokitten) library.
103+
The recording starts automatically upon the first interaction with the device and stops when the test is complete.
104+
105+
**Configuration:**
106+
107+
- **`true`**: Enables video recording with default settings.
108+
- **`false`** (default): Disables video recording.
109+
- **`DetoxAllure2AdapterDeviceVideoOptions`** (object): Enables recording and provides fine-grained control over the settings.
110+
111+
- `saveAll: boolean` (default: `false`): If `true`, saves videos for all tests. By default, only videos for failed tests are kept.
112+
- `ios: Partial<VideokittenOptionsIOS>`: Custom options for iOS, as defined by `videokitten`.
113+
- `android: Partial<VideokittenOptionsAndroid>`: Custom options for Android, as defined by `videokitten`.
114+
115+
**Example with custom options:**
116+
117+
```js
118+
// jest.config.js
119+
module.exports = {
120+
eventListeners: [
121+
'jest-metadata/environment-listener',
122+
'jest-allure2-reporter/environment-listener',
123+
['detox-allure2-adapter', {
124+
useSteps: true,
125+
deviceLogs: {
126+
saveAll: true,
127+
ios: (entry) => entry.level === 'error',
128+
android: (entry) => entry.priority === 'E',
129+
},
130+
deviceScreenshots: {
131+
saveAll: true,
132+
},
133+
deviceVideos: {
134+
saveAll: true,
135+
ios: {
136+
codec: 'hevc',
137+
},
138+
android: {
139+
bitRate: 4_000_000,
140+
}
141+
}
142+
}],
143+
],
144+
},
145+
```
146+
147+
Refer to the [`videokitten` documentation](https://www.npmjs.com/package/videokitten) for a full list of options for each platform.
148+
65149
## Running Tests
66150

67151
After making these changes, you can run your tests as usual. The tests will run with Detox and Jest, and the results will be reported using Allure. Configure your `npm test` script in the `package.json` file to run your Detox tests.

package-e2e/test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { ReporterOptions } from 'jest-allure2-reporter';
22

33
import listener from 'detox-allure2-adapter';
4-
import type { DetoxAllure2AdapterOptions, DetoxAllure2AdapterDeviceLogsOptions, DetoxAllure2AdapterDeviceScreenshotOptions } from 'detox-allure2-adapter';
4+
import type {
5+
DetoxAllure2AdapterOptions,
6+
DetoxAllure2AdapterDeviceLogsOptions,
7+
DetoxAllure2AdapterDeviceScreenshotOptions,
8+
DetoxAllure2AdapterDeviceVideoOptions,
9+
} from 'detox-allure2-adapter';
510
import DetoxAllurePathBuilder from 'detox-allure2-adapter/path-builder';
611
import presetAllure from 'detox-allure2-adapter/preset-allure';
712
import presetDetox from 'detox-allure2-adapter/preset-detox';
@@ -21,6 +26,7 @@ assertType<DetoxAllure2AdapterOptions>({
2126
useSteps: true,
2227
deviceLogs: true,
2328
deviceScreenshots: true,
29+
deviceVideos: true,
2430
});
2531

2632
assertType<DetoxAllure2AdapterDeviceLogsOptions>({
@@ -38,3 +44,15 @@ assertType<DetoxAllure2AdapterDeviceLogsOptions>({
3844
assertType<DetoxAllure2AdapterDeviceScreenshotOptions>({
3945
saveAll: true,
4046
});
47+
48+
assertType<DetoxAllure2AdapterDeviceVideoOptions>({
49+
saveAll: true,
50+
ios: {
51+
codec: 'hevc',
52+
},
53+
android: {
54+
recording: {
55+
bitRate: 4_000_000,
56+
},
57+
},
58+
});

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"dependencies": {
110110
"archiver": "^6.0.1",
111111
"logkitten": "^1.3.0",
112-
"screenkitten": "^1.0.0"
112+
"screenkitten": "^1.0.0",
113+
"videokitten": "^1.0.1"
113114
}
114115
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type {
22
DetoxAllure2AdapterOptions,
33
DetoxAllure2AdapterDeviceLogsOptions,
44
DetoxAllure2AdapterDeviceScreenshotOptions,
5+
DetoxAllure2AdapterDeviceVideoOptions,
56
} from './types';
67

78
export { listener as default } from './listener';

src/listener.ts

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,38 @@ import { LogBuffer } from './logs';
1515
import { ScreenshotHelper } from './screenshots';
1616
import { wrapWithSteps } from './steps';
1717
import type { DetoxAllure2AdapterOptions } from './types';
18-
import { DeviceWrapper, WorkerWrapper } from './utils';
18+
import { DeviceWrapper, WorkerWrapper, once } from './utils';
19+
import { VideoManager } from './video';
1920

2021
export const listener: EnvironmentListenerFn = (
2122
{ testEvents },
2223
{
2324
useSteps = false,
2425
deviceLogs = false,
2526
deviceScreenshots = false,
27+
deviceVideos = false,
2628
onError,
2729
}: DetoxAllure2AdapterOptions = {},
2830
) => {
2931
let logHandler: ReturnType<typeof createLogHandler>;
3032
let zipHandler: ReturnType<typeof createZipHandler>;
3133
let inferMimeType: MIMEInferer;
32-
let $test: ReturnType<typeof allure.$bind> | undefined;
33-
let artifactsManager: any;
34+
let workerWrapper: WorkerWrapper | undefined;
3435
let logs: LogBuffer | undefined;
3536
let screenshots: ScreenshotHelper | undefined;
37+
let videoManager: VideoManager | undefined;
38+
39+
let $test: ReturnType<typeof allure.$bind> | undefined;
40+
let $hook: ReturnType<typeof allure.$bind> | undefined;
41+
let failing = false;
42+
43+
const flushArtifacts = once(async () => {
44+
await workerWrapper?.artifactsManager?._idlePromise;
45+
await Promise.all([logs?.close(), videoManager?.stopAndAttach($hook, failing)]);
46+
workerWrapper = undefined;
47+
logs = undefined;
48+
videoManager = undefined;
49+
});
3650

3751
testEvents
3852
.on('setup', () => {
@@ -42,7 +56,7 @@ export const listener: EnvironmentListenerFn = (
4256
inferMimeType = context.inferMimeType;
4357
});
4458

45-
const workerWrapper = new WorkerWrapper(worker);
59+
workerWrapper = new WorkerWrapper(worker);
4660
workerWrapper.artifactsManager.on('trackArtifact', onTrackArtifact);
4761

4862
const device = new DeviceWrapper(detox.device);
@@ -65,43 +79,60 @@ export const listener: EnvironmentListenerFn = (
6579
onError,
6680
});
6781
}
82+
83+
if (deviceVideos) {
84+
const baseOptions = deviceVideos === true ? {} : deviceVideos;
85+
const effectiveOptions = useSteps ? baseOptions : { ...baseOptions, lazyStart: false };
86+
videoManager = new VideoManager({ device, options: effectiveOptions });
87+
}
6888
})
6989
.on('setup', async () => {
7090
if (useSteps) {
71-
wrapWithSteps({ detox, worker, allure, logs, screenshots });
91+
wrapWithSteps({ detox, worker, allure, logs, screenshots, videoManager });
7292
}
7393
})
74-
.on('test_start', () => {
94+
.on('run_start', async () => {
95+
// Only start early if configured (lazyStart === false)
96+
await videoManager?.ensureRecordingEager();
97+
})
98+
.on('test_started', async () => {
99+
// Start recording eagerly if configured or when not using step wrappers
100+
await videoManager?.ensureRecordingEager();
101+
})
102+
.on('test_start', async () => {
75103
$test = allure.$bind();
104+
$hook = undefined;
76105
logs?.attachBefore(allure);
106+
failing = false;
77107
})
78-
.on('hook_start', () => {
108+
.on('hook_start', async ({ event }) => {
79109
logs?.attachBefore(allure);
110+
111+
if (event.hook.type === 'beforeAll' || event.hook.type === 'afterAll') {
112+
$hook ??= allure.$bind();
113+
await videoManager?.ensureRecordingEager();
114+
}
80115
})
81116
.on('hook_failure', async () => {
117+
failing = true;
82118
await Promise.all([logs?.attachAfterFailure(allure), screenshots?.attachFailure(allure)]);
83119
})
84120
.on('hook_success', async () => {
85121
await Promise.all([logs?.attachAfterSuccess(allure), screenshots?.attachSuccess(allure)]);
86122
})
87123
.on('test_fn_failure', async () => {
124+
failing = true;
88125
await Promise.all([logs?.attachAfterFailure(allure), screenshots?.attachFailure(allure)]);
89126
})
90127
.on('test_fn_success', async () => {
91128
await Promise.all([logs?.attachAfterSuccess(allure), screenshots?.attachSuccess(allure)]);
92129
})
93130
.on('test_done', async () => {
131+
await videoManager?.stopAndAttach($test, failing);
94132
$test = undefined;
95133
})
96-
.on('teardown', flushArtifacts, -1)
97-
.on('test_environment_teardown', flushArtifacts, -1);
98-
99-
async function flushArtifacts() {
100-
await artifactsManager?._idlePromise;
101-
await logs?.close();
102-
artifactsManager = undefined;
103-
logs = undefined;
104-
}
134+
.once('teardown', flushArtifacts, -1)
135+
.once('test_environment_teardown', flushArtifacts, -1);
105136

106137
function onTrackArtifact(artifact: any) {
107138
const $step = allure.$bind();

src/steps/wrapWithSteps.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { AllureRuntime } from 'jest-allure2-reporter/api';
33
import { type StepLogRecorder } from '../logs';
44
import { type ScreenshotHelper } from '../screenshots';
5+
import { type VideoManager } from '../video';
56
import { androidDescriptionMaker, iosDescriptionMaker } from './description-maker';
67
import type { StepDescriptionMaker } from './description-maker';
78

@@ -11,10 +12,11 @@ export interface WrapWithStepsOptions {
1112
allure: AllureRuntime;
1213
logs?: StepLogRecorder;
1314
screenshots?: ScreenshotHelper;
15+
videoManager?: VideoManager;
1416
}
1517

1618
export function wrapWithSteps(options: WrapWithStepsOptions) {
17-
const { detox, worker, allure, logs, screenshots } = options;
19+
const { detox, worker, allure, logs, screenshots, videoManager } = options;
1820
const { device } = detox;
1921
const platform = device.getPlatform();
2022

@@ -57,6 +59,7 @@ export function wrapWithSteps(options: WrapWithStepsOptions) {
5759
? allure.step(desc.message, async () => {
5860
if (desc.args) allure.parameters(desc.args);
5961
logs?.attachBefore(allure);
62+
await videoManager?.ensureRecording();
6063

6164
try {
6265
const result = await send(...args);
@@ -84,7 +87,7 @@ function initDescriptionMaker(platform: string): StepDescriptionMaker | undefine
8487
}
8588

8689
function wrapDeviceMethod(
87-
{ detox, allure, logs, screenshots }: WrapWithStepsOptions,
90+
{ detox, allure, logs, screenshots, videoManager }: WrapWithStepsOptions,
8891
methodName: string,
8992
stepDescription: string,
9093
) {
@@ -93,6 +96,8 @@ function wrapDeviceMethod(
9396
if (typeof originalMethod !== 'function') return;
9497

9598
device[methodName] = async (...args: any[]) => {
99+
await videoManager?.ensureRecording();
100+
96101
return await allure.step(stepDescription, async () => {
97102
try {
98103
logs?.attachBefore(allure);

src/types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AndroidEntry, IosEntry } from 'logkitten';
2+
import type { VideokittenOptionsIOS, VideokittenOptionsAndroid } from 'videokitten';
23

34
export type DetoxAllure2AdapterOptions = {
45
/**
@@ -14,6 +15,10 @@ export type DetoxAllure2AdapterOptions = {
1415
* Device screenshots configuration for per-step logging
1516
*/
1617
deviceScreenshots?: boolean | DetoxAllure2AdapterDeviceScreenshotOptions;
18+
/**
19+
* Device video recording for failed tests
20+
*/
21+
deviceVideos?: boolean | DetoxAllure2AdapterDeviceVideoOptions;
1722
/**
1823
* Callback to handle errors
1924
*/
@@ -33,5 +38,28 @@ export interface DetoxAllure2AdapterDeviceLogsOptions {
3338
}
3439

3540
export interface DetoxAllure2AdapterDeviceScreenshotOptions {
41+
/**
42+
* Whether to save all screenshots
43+
* @default false
44+
*/
3645
saveAll?: boolean;
3746
}
47+
48+
export interface DetoxAllure2AdapterDeviceVideoOptions {
49+
/**
50+
* Whether to save all videos
51+
* @default false
52+
*/
53+
saveAll?: boolean;
54+
/**
55+
* Controls when video recording starts.
56+
* - If `true` (default), recording begins lazily on the first device interaction (step).
57+
* - If `false`, recording starts immediately at the beginning of each test.
58+
*
59+
* This option is only effective when `useSteps` is enabled.
60+
* @default true
61+
*/
62+
lazyStart?: boolean;
63+
ios?: Partial<VideokittenOptionsIOS>;
64+
android?: Partial<VideokittenOptionsAndroid>;
65+
}

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './deferred';
22
export * from './device-wrapper';
3+
export * from './once';
34
export * from './worker-wrapper';

src/utils/once.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function once<T>(fn: () => T): () => T {
2+
let result: T | undefined;
3+
let called = false;
4+
5+
return () => {
6+
if (called) {
7+
return result!;
8+
}
9+
10+
called = true;
11+
result = fn();
12+
return result;
13+
};
14+
}

0 commit comments

Comments
 (0)