detox-allure2-adapter
Version:
Detox adapter for jest-allure2-reporter
291 lines (254 loc) • 9.24 kB
text/typescript
// eslint-disable-next-line import/no-internal-modules
import type { AllureRuntime } from 'jest-allure2-reporter/api';
import { type StepLogRecorder } from '../logs';
import { type ScreenshotHelper } from '../screenshots';
import type { DetoxTestFailedResult, WorkerWrapper } from '../utils';
import { type VideoManager } from '../video';
import { type ViewHierarchyHelper } from '../view-hierarchy';
import { androidDescriptionMaker, iosDescriptionMaker } from './description-maker';
import type { StepDescriptionMaker } from './description-maker';
export interface WrapWithStepsOptions {
detox: typeof import('detox');
worker: WorkerWrapper;
allure: AllureRuntime;
logs?: StepLogRecorder;
screenshots?: ScreenshotHelper;
videoManager?: VideoManager;
viewHierarchy?: ViewHierarchyHelper;
userArtifacts: 'ignore' | 'copy' | 'move';
}
interface WrapWithDescriptionMakerOptions extends WrapWithStepsOptions {
descriptionMaker: StepDescriptionMaker;
send: (...args: any[]) => Promise<{ type?: string }>;
}
interface WrapWithScreenshotTakingOptions {
// eslint-disable-next-line @typescript-eslint/ban-types
method: Function;
screenshots?: ScreenshotHelper;
allure: AllureRuntime;
}
export function wrapWithSteps(options: WrapWithStepsOptions) {
const { allure, detox, worker, userArtifacts } = options;
const { device } = detox;
const platform = device.getPlatform();
// Wrap device methods using the helper
wrapDeviceMethod(options, 'launchApp', 'Launch app');
wrapDeviceMethod(options, 'relaunchApp', 'Relaunch app');
wrapDeviceMethod(options, 'openURL', 'Open URL');
wrapDeviceMethod(options, 'reloadReactNative', 'Reload React Native');
wrapDeviceMethod(options, 'sendToHome', 'Send app to background');
wrapDeviceMethod(options, 'setOrientation', 'Set orientation');
wrapDeviceMethod(options, 'matchFace', 'Match face');
wrapDeviceMethod(options, 'unmatchFace', 'Unmatch face');
wrapDeviceMethod(options, 'matchFinger', 'Match finger');
wrapDeviceMethod(options, 'unmatchFinger', 'Unmatch finger');
device.installApp = allure.createStep('Install app', device.installApp.bind(device));
device.resetContentAndSettings = allure.createStep(
'Reset content and settings',
device.resetContentAndSettings.bind(device),
);
device.uninstallApp = allure.createStep('Uninstall app', device.uninstallApp.bind(device));
device.terminateApp = allure.createStep('Terminate app', device.terminateApp.bind(device));
device.selectApp = allure.createStep('Select app {{0}}', device.selectApp.bind(device));
if (typeof device.resetAppState === 'function') {
device.resetAppState = allure.createStep('Reset app state', device.resetAppState.bind(device));
}
if (userArtifacts === 'copy' || userArtifacts === 'move') {
device.takeScreenshot = allure.createFileAttachment(device.takeScreenshot.bind(device), {
name: '{{firstOr "screenshot"}}.png',
handler: userArtifacts === 'move' ? 'mv-delayed' : 'copy',
});
device.takeScreenshot = allure.createStep('Take screenshot', [null], device.takeScreenshot);
device.captureViewHierarchy = allure.createFileAttachment(
device.captureViewHierarchy.bind(device),
{
name: '{{firstOr "capture"}}.viewhierarchy.zip',
mimeType: 'application/zip',
handler: userArtifacts === 'move' ? 'zip-rm' : 'zip',
},
);
device.captureViewHierarchy = allure.createStep(
'Capture view hierarchy',
[null],
device.captureViewHierarchy,
);
if (typeof device.generateViewHierarchyXml === 'function') {
device.generateViewHierarchyXml = allure.createAttachment(
device.generateViewHierarchyXml.bind(device),
{
name: 'viewhierarchy.xml',
mimeType: 'application/xml',
},
);
device.generateViewHierarchyXml = allure.createStep(
'Generate view hierarchy XML',
['shouldInjectTestIds'],
device.generateViewHierarchyXml,
);
}
}
wrapPilotMethod(options);
const descriptionMaker = initDescriptionMaker(platform);
if (descriptionMaker) {
const ws = worker.asyncWebSocket;
ws.send = wrapSendMethod({
...options,
descriptionMaker,
send: ws.send.bind(ws) as (...args: any[]) => Promise<{ type?: string }>,
});
const xcuitestRunner = worker.xcuitestRunner;
if (xcuitestRunner) {
xcuitestRunner.execute = wrapSendMethod({
...options,
descriptionMaker,
send: xcuitestRunner.execute.bind(xcuitestRunner),
});
}
}
}
function initDescriptionMaker(platform: string): StepDescriptionMaker | undefined {
if (platform === 'ios') {
return iosDescriptionMaker;
} else if (platform === 'android') {
return androidDescriptionMaker;
}
return undefined;
}
function wrapDeviceMethod(
{ detox, allure, logs, screenshots, videoManager }: WrapWithStepsOptions,
methodName: string,
stepDescription: string,
) {
const device = detox.device as any;
const originalMethod = device[methodName];
if (typeof originalMethod !== 'function') return;
device[methodName] = async (...args: any[]) => {
await videoManager?.ensureRecording();
return await allure.step(stepDescription, async () => {
try {
logs?.attachBefore(allure);
const result = await originalMethod.apply(device, args);
await Promise.all([logs?.attachAfterSuccess(allure), screenshots?.attach(allure, false)]);
return result;
} catch (error) {
await Promise.all([logs?.attachAfterFailure(allure), screenshots?.attachFailure(allure)]);
throw error; // Re-throw the error
}
});
};
}
function wrapPilotMethod({ detox, allure, screenshots }: WrapWithStepsOptions) {
const pilot = (detox as any).pilot;
const originalInit = pilot?.init;
if (typeof originalInit !== 'function') return;
pilot.init = function () {
// eslint-disable-next-line prefer-rest-params
const result = Reflect.apply(originalInit, this, arguments);
const instance = pilot.pilot;
if (typeof instance?.performStep === 'function') {
instance.performStep = allure.createStep(
'{{0}}',
[null],
wrapWithScreenshotTaking({
method: instance.performStep.bind(instance),
screenshots,
allure,
}),
);
}
if (typeof instance?.autopilot === 'function') {
instance.autopilot = allure.createStep(
'{{0}}',
[null],
wrapWithScreenshotTaking({
method: instance.autopilot.bind(instance),
screenshots,
allure,
}),
);
}
return result;
};
}
function wrapWithScreenshotTaking({
method,
screenshots,
allure,
}: WrapWithScreenshotTakingOptions) {
if (!screenshots || !allure) {
return method;
}
return async (...args: any[]) => {
try {
return await method(...args);
} catch (error) {
if (`${error}`.startsWith('Error:')) {
await screenshots?.attachFailure(allure);
}
throw error;
}
};
}
function wrapSendMethod({
descriptionMaker,
allure,
logs,
videoManager,
screenshots,
viewHierarchy,
send,
}: WrapWithDescriptionMakerOptions) {
const onActionSuccess = async () => {
await logs?.attachAfterSuccess(allure);
};
const onActionFailure = async (shouldSetStatus: boolean, result?: DetoxTestFailedResult) => {
if (shouldSetStatus) {
allure.status('failed');
if (result?.params) {
const { NSLocalizedDescription, details, DetoxFailureInformation } = result.params;
let message = NSLocalizedDescription || details;
if (message) {
let trace = DetoxFailureInformation
? `${DetoxFailureInformation.functionName || 'unknown'} at ${DetoxFailureInformation.file || 'unknown'}:${DetoxFailureInformation.lineNumber || 'unknown'}`
: undefined;
if (message.includes('\n')) {
const [first, ...rest] = message.split('\n');
if (trace) {
rest.push(trace);
}
message = first;
trace = rest.join('\n').trimStart();
}
allure.statusDetails({ message, trace });
}
}
}
const [attachmentResult] = await Promise.all([
viewHierarchy?.attachFromResult(allure, result),
logs?.attachAfterFailure(allure),
]);
if (!attachmentResult?.screenshotsAttached) {
await screenshots?.attachFailure(allure);
}
};
return async (...args: any[]) => {
const desc = descriptionMaker(args[0]);
return desc?.message
? allure.step(desc.message, async () => {
if (desc.args) allure.parameters(desc.args);
logs?.attachBefore(allure);
await videoManager?.ensureRecording();
try {
const result = await send(...args);
await (result?.type === 'testFailed'
? onActionFailure(true, result as DetoxTestFailedResult)
: onActionSuccess());
return result;
} catch (error) {
await onActionFailure(false);
throw error;
}
})
: send(...args);
};
}