Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"unicorn/no-array-reduce": "off",
"unicorn/no-keyword-prefix": "off",
"unicorn/no-null": "off",
"unicorn/no-static-only-class": "off",
"unicorn/no-this-assignment": "off",
"unicorn/no-unused-properties": "off",
"unicorn/prefer-module": "off"
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,5 @@ dist
.idea/

!__fixtures__
.DS_Store
/.ai
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
"*.mjs",
"!.*.js",
"!jest.config.js",
"!**/__utils__",
"!**/__tests__",
"!**/__*__",
"!**/*.test.*"
],
"exports": {
Expand Down
1,525 changes: 1,525 additions & 0 deletions src/steps/description-maker/android/__fixtures__/merged-detox-traces.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

255 changes: 255 additions & 0 deletions src/steps/description-maker/android/android-description-maker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// eslint-disable-next-line import/no-internal-modules
import _mergedTraces from './__fixtures__/merged-detox-traces.json';
import { androidDescriptionMaker } from './android-description-maker';

const mergedTraces = _mergedTraces as any[];

describe('Android description maker', () => {
test('should handle basic tap by test ID', () => {
const payload = {
type: 'invoke',
params: {
target: { type: 'Class', value: 'com.wix.detox.espresso.EspressoDetox' },
method: 'perform',
args: [
{
type: 'Invocation',
value: {
target: { type: 'Class', value: 'com.wix.detox.espresso.DetoxMatcher' },
method: 'matcherForTestId',
args: ['SimpleButton', { type: 'Boolean', value: false }],
},
},
{
type: 'Invocation',
value: {
target: { type: 'Class', value: 'com.wix.detox.espresso.DetoxViewActions' },
method: 'click',
args: [],
},
},
],
},
};

const description = androidDescriptionMaker(payload);
expect(description).toEqual({
message: 'Click on #SimpleButton',
args: { id: 'SimpleButton' },
});
});

const testCases = mergedTraces.map((trace, index) => [index, trace]);
test.each(testCases)('should process trace %i: %j', (_index, trace) => {
// if (_index === 0) { debugger; }
expect(androidDescriptionMaker(trace)).toMatchSnapshot();
});

test.each([
['string', 'not an object'],
['number', 42],
['null', null],
['undefined', undefined],
['empty object', {}],
['unknown type', { type: 'unknown' }],
])('should ignore invalid invocation: %s', (_label, invocation) => {
expect(androidDescriptionMaker(invocation)).toBeNull();
});

test('should ignore invocation without target', () => {
const payload = {
type: 'invoke',
params: {
method: 'matcherForTestId',
},
};
expect(androidDescriptionMaker(payload)).toBeNull();
});

it('should ignore invocation without params', () => {
const payload: any = {
type: 'invoke',
params: {},
};
expect(androidDescriptionMaker(payload)).toBeNull();

delete payload.params;
expect(androidDescriptionMaker(payload)).toBeNull();
});

test('should handle unknown class in registry', () => {
const payload = {
type: 'invoke',
params: {
target: { type: 'Class', value: 'com.wix.detox.NonExistentClass' },
method: 'someMethod',
args: [],
},
};
expect(androidDescriptionMaker(payload)).toBeNull();
});

test('should handle non-existent method on valid class', () => {
const payload = {
type: 'invoke',
params: {
target: { type: 'Class', value: 'com.wix.detox.espresso.DetoxMatcher' },
method: 'nonExistentMethod',
args: [],
},
};
expect(androidDescriptionMaker(payload)).toBeNull();
});

test('should handle malformed class object', () => {
const payload = {
type: 'invoke',
params: {
target: { type: 'NotAClass', value: 'something' },
method: 'method',
args: [],
},
};
expect(androidDescriptionMaker(payload)).toBeNull();
});

test('should handle null target in invocation', () => {
const payload = {
type: 'invoke',
params: {
target: null,
method: 'method',
args: [],
},
};
expect(androidDescriptionMaker(payload)).toBeNull();
});

test('should handle deeply nested null values', () => {
const payload = {
type: 'invoke',
params: {
target: {
type: 'Invocation',
value: {
target: null,
method: 'method',
args: [],
},
},
method: 'method',
args: [],
},
};
expect(androidDescriptionMaker(payload)).toBeNull();
});

test('should handle malformed primitive type object', () => {
const payload = {
type: 'invoke',
params: {
target: { type: 'Class', value: 'com.wix.detox.espresso.DetoxMatcher' },
method: 'matcherForTestId',
args: [
{ type: 'MalformedType', value: 'test' },
{ type: 'Boolean', value: false },
],
},
};

expect(androidDescriptionMaker(payload)).toEqual({
message: '##ERROR!',
args: { id: '#ERROR!' },
});
});

test('should handle array with null values', () => {
const payload = {
type: 'invoke',
params: {
target: { type: 'Class', value: 'com.wix.detox.espresso.EspressoDetox' },
method: 'perform',
args: [
{
type: 'Invocation',
value: {
target: { type: 'Class', value: 'com.wix.detox.espresso.DetoxMatcher' },
method: 'matcherForTestId',
args: [],
},
},
{
type: 'Invocation',
value: {
target: { type: 'Class', value: 'unknownClass' },
method: 'click',
args: [],
},
},
],
},
};
expect(androidDescriptionMaker(payload)).toBeNull();
});

test('should handle unknown message type', () => {
const payload = {
type: 'unknownType',
params: {},
};
expect(androidDescriptionMaker(payload)).toBeNull();
});

test('should handle circular references', () => {
const circular: any = {
type: 'invoke',
params: {
target: { type: 'Class', value: 'com.wix.detox.espresso.DetoxMatcher' },
method: 'method',
args: [],
},
};
circular.params.args.push(circular);
expect(androidDescriptionMaker(circular)).toBeNull();
});

test('should handle empty invocation value', () => {
const payload = {
type: 'invoke',
params: {
target: {
type: 'Invocation',
value: {},
},
method: 'method',
args: [],
},
};
expect(androidDescriptionMaker(payload)).toBeNull();
});

test('should handle malformed matcher chain', () => {
const payload = {
type: 'invoke',
params: {
target: { type: 'Class', value: 'com.wix.detox.espresso.DetoxMatcher' },
method: 'matcherWithAncestor',
args: [
{
type: 'Invocation',
value: {
target: null,
method: 'method',
args: [],
},
},
null,
],
},
};
expect(androidDescriptionMaker(payload)).toEqual({
message: 'with ancestor',
args: null,
});
});
});
13 changes: 13 additions & 0 deletions src/steps/description-maker/android/android-description-maker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { StepDescription, StepDescriptionMaker } from '../types';
import { AndroidDescriptionProcessor } from './core';

/**
* The main entry point for the Android Description Maker.
* Implements a pseudo-compiler pattern to process invocation trees.
*/
export const androidDescriptionMaker: StepDescriptionMaker = (
payload: unknown,
): StepDescription | null => {
const processor = new AndroidDescriptionProcessor();
return processor.process(payload);
};
32 changes: 32 additions & 0 deletions src/steps/description-maker/android/classes/Detox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { StepDescription } from '../../types';
import { msg } from '../../utils';

export class Detox {
static setUpCustomEspressoIdlingResources(): StepDescription {
return msg('Set up custom Espresso idling resources');
}

static runDetoxTests(): StepDescription {
return msg('Run Detox tests');
}

static launchMainActivity(): StepDescription {
return msg('Launch main activity');
}

static startActivityFromUrl(url: string): StepDescription {
return msg(`Start activity from URL: ${url}`);
}

static startActivityFromNotification(dataFilePath: string): StepDescription {
return msg(`Start activity from notification: ${dataFilePath}`);
}

static getAppContext(): StepDescription {
return msg('Get application context');
}

static generateViewHierarchyXml(shouldInjectTestIds: boolean): StepDescription {
return msg(`Generate view hierarchy XML`, { inject_test_ids: shouldInjectTestIds });
}
}
Loading