import { Constants } from 'expo-constants';
import React from 'react';
import symbolicateStackTrace from 'react-native/Libraries/Core/Devtools/symbolicateStackTrace';
import TestRenderer from 'react-test-renderer';
import LogSerialization from '../LogSerialization';
import { LogErrorData } from '../RemoteLogging';
jest.mock('react-native/Libraries/Core/Devtools/symbolicateStackTrace', () =>
jest.fn(
async stack =>
// We don't test symbolication itself in this test, partly because it's complex
stack
)
);
jest.mock('expo-constants', () => ({
Constants: {
manifest: {
developer: {
projectRoot: '/home/test/project',
},
},
},
}));
it(`serializes simple log messages`, async () => {
let result = await LogSerialization.serializeLogDataAsync(
['hi', 1, true, 0, '', null, false, undefined],
'info'
);
expect(result.body).toEqual(['hi', '1', 'true', '0', '', 'null', 'false', 'undefined']);
expect(result.includesStack).toBeFalsy();
});
it(`serializes nested objects`, async () => {
let result = await LogSerialization.serializeLogDataAsync(
[{ outer: { inner: [[], {}] } }],
'info'
);
expect(result.body).toMatchSnapshot();
expect(result.includesStack).toBeFalsy();
});
it(`serializes functions`, async () => {
let result = await LogSerialization.serializeLogDataAsync([function test() {}, () => {}], 'info');
expect(result.body).toEqual(['[Function test]', '[Function anonymous]']);
expect(result.includesStack).toBeFalsy();
});
it(`serializes symbols`, async () => {
let result = await LogSerialization.serializeLogDataAsync(
[Symbol('test'), Symbol.iterator],
'info'
);
expect(result.body).toEqual(['Symbol(test)', 'Symbol(Symbol.iterator)']);
expect(result.includesStack).toBeFalsy();
});
it(`serializes promises`, async () => {
let result = await LogSerialization.serializeLogDataAsync(
[Promise.resolve('test'), Promise.reject(new Error('Expected'))],
'info'
);
expect(result.body.length).toBe(2);
expect(result.body[0]).toMatch('Promise');
expect(result.body[1]).toMatch('Promise');
expect(result.includesStack).toBeFalsy();
});
it(`serializes React elements`, async () => {
class TestComponent extends React.Component {
render () {
return ;
}
}
let result = await LogSerialization.serializeLogDataAsync([], 'info');
expect(Array.isArray(result.body)).toBe(true);
expect(result.includesStack).toBeFalsy();
});
it(`serializes React components (refs)`, async () => {
class TestComponent extends React.Component {
child: React.Component | null = null;
render() {
return (this.child = component)} />;
}
}
class ChildComponent extends React.Component {
render() {
return 'test';
}
}
let testRenderer = TestRenderer.create();
let result = await LogSerialization.serializeLogDataAsync(
[testRenderer.root.instance.child],
'info'
);
expect(Array.isArray(result.body)).toBe(true);
expect(result.body[0]).toMatch('ChildComponent');
expect(result.includesStack).toBeFalsy();
});
describe('with stack trace support in Expo CLI', () => {
it(`includes a symbolicated stack trace when logging an error`, async () => {
let mockError = _getMockError('Test error');
let result = await LogSerialization.serializeLogDataAsync([mockError], 'info');
expect(symbolicateStackTrace).toHaveBeenCalledTimes(1);
expect(Array.isArray(result.body)).toBe(true);
expect(result.body.length).toBe(1);
expect((result.body[0] as LogErrorData).message).toBe('Test error');
expect((result.body[0] as LogErrorData).stack).toMatchSnapshot();
expect(result.includesStack).toBe(true);
});
it(`can symbolicate errors from V8`, async () => {
let mockError = _getMockV8Error('Test error');
let result = await LogSerialization.serializeLogDataAsync([mockError], 'info');
expect(symbolicateStackTrace).toHaveBeenCalledTimes(1);
expect(Array.isArray(result.body)).toBe(true);
expect(result.body.length).toBe(1);
expect((result.body[0] as LogErrorData).message).toBe('Test error');
expect((result.body[0] as LogErrorData).stack).toMatch('_exampleFunction');
expect(result.includesStack).toBe(true);
});
it(`includes a symbolicated stack trace when warning`, async () => {
let result = await LogSerialization.serializeLogDataAsync(['warning message'], 'warn');
expect(symbolicateStackTrace).toHaveBeenCalledTimes(1);
expect(Array.isArray(result.body)).toBe(true);
expect(result.body.length).toBe(1);
expect((result.body[0] as LogErrorData).message).toBe('warning message');
expect((result.body[0] as LogErrorData).stack).toBeDefined();
expect(result.includesStack).toBe(true);
});
it(`includes a symbolicated stack trace when erroring`, async () => {
let result = await LogSerialization.serializeLogDataAsync(['error message'], 'error');
expect(symbolicateStackTrace).toHaveBeenCalledTimes(1);
expect(Array.isArray(result.body)).toBe(true);
expect(result.body.length).toBe(1);
expect((result.body[0] as LogErrorData).message).toBe('error message');
expect((result.body[0] as LogErrorData).stack).toBeDefined();
expect(result.includesStack).toBe(true);
});
it(`uses the provided error's stack trace when erroring`, async () => {
let mockError = _getMockError('Test error');
let result = await LogSerialization.serializeLogDataAsync([mockError], 'error');
expect(symbolicateStackTrace).toHaveBeenCalledTimes(1);
expect(Array.isArray(result.body)).toBe(true);
expect(result.body.length).toBe(1);
expect((result.body[0] as LogErrorData).message).toBe('Test error');
expect((result.body[0] as LogErrorData).stack).toMatch('_exampleFunction');
});
it(`symbolicates unhandled promise rejections`, async () => {
let warningMessage = _getMockUnhandledPromiseRejection();
let result = await LogSerialization.serializeLogDataAsync([warningMessage], 'warn');
expect(symbolicateStackTrace).toHaveBeenCalledTimes(1);
expect(Array.isArray(result.body)).toBe(true);
expect(result.body.length).toBe(1);
expect((result.body[0] as LogErrorData).message).toMatch('Unhandled promise rejection: ');
expect((result.body[0] as LogErrorData).stack).toBeDefined();
expect(result.includesStack).toBe(true);
});
it(`doesn't fail if the error has no stack frames`, async () => {
let mockError = new Error('Test error');
mockError.stack = mockError.stack!.split('\n')[0];
let result = await LogSerialization.serializeLogDataAsync([mockError], 'info');
expect(result).toMatchSnapshot();
});
it(`doesn't fail if the error stack property is missing`, async () => {
let mockError = new Error('Test error');
mockError.stack = undefined;
let result = await LogSerialization.serializeLogDataAsync([mockError], 'info');
expect(result).toMatchSnapshot();
});
it(`doesn't fail if symbolication fails`, async () => {
(symbolicateStackTrace as jest.Mock).mockImplementationOnce(async () => {
throw new Error('Intentional symbolication error');
});
let mockError = _getMockError('Test error');
let result = await LogSerialization.serializeLogDataAsync([mockError], 'error');
expect(symbolicateStackTrace).toHaveBeenCalledTimes(1);
expect(Array.isArray(result.body)).toBe(true);
expect(result.body.length).toBe(1);
expect((result.body[0] as LogErrorData).message).toBe('Test error');
expect((result.body[0] as LogErrorData).stack).toMatchSnapshot();
expect(result.includesStack).toBe(true);
});
});
describe(`without stack trace support in Expo CLI`, () => {
let originalProjectRoot;
beforeAll(() => {
originalProjectRoot = Constants.manifest.developer.projectRoot;
delete Constants.manifest.developer.projectRoot;
});
afterAll(() => {
Constants.manifest.developer.projectRoot = originalProjectRoot;
});
it(`doesn't capture a stack trace`, async () => {
let result = await LogSerialization.serializeLogDataAsync(['oh no'], 'error');
expect(result.includesStack).toBeFalsy();
expect(symbolicateStackTrace).not.toHaveBeenCalled();
});
});
function _getMockError(message) {
let error = new Error(message);
error.stack = `_exampleFunction@/home/test/project/App.js:125:13
_depRunCallbacks@/home/test/project/node_modules/dep/index.js:77:45
tryCallTwo@/home/test/project/node_modules/react-native/node_modules/promise/lib/core.js:45:5
doResolve@/home/test/project/node_modules/react-native/node_modules/promise/lib/core.js:200:13`;
return error;
}
function _getMockV8Error(message) {
let error = new Error(message);
let mockStack = `
at _exampleFunction (/home/test/project/App.js:125:13)
at _depRunCallbacks (/home/test/project/node_modules/dep/index.js:77:45)
at tryCallTwo (/home/test/project/node_modules/react-native/node_modules/promise/lib/core.js:45:5)
at doResolve (/home/test/project/node_modules/react-native/node_modules/promise/lib/core.js:200:13)`;
error.stack = error.stack!.split('\n')[0] + mockStack;
return error;
}
function _getMockUnhandledPromiseRejection() {
return `Possible Unhandled Promise Rejection (id: 0):
Error: Intentionally unhandled async error
_callee$@http://localhost:19001/entry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/home/test/project/node_modules/expo/tools/hashAssetFiles:99344:32
tryCatch@http://localhost:19001/entry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/home/test/project/node_modules/expo/tools/hashAssetFiles:12301:44
invoke@http://localhost:19001/entry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/home/test/project/node_modules/expo/tools/hashAssetFiles:12489:30`;
}