RN: A wild YellowBox has appeared!

Summary:
Replaces the existing `YellowBox` with a modern one.

Here are the notable changes:

- Sort warnings by recency (with most recent on top).
- Group warnings by format string if present.
- Present stack traces similar to RedBox.
- Show status of loading source maps.
- Support inspecting each occurrence of a warning.
- Fixed a bunch of edge cases and race conditions.

Reviewed By: TheSavior

Differential Revision: D8345180

fbshipit-source-id: b9e10d526b262c3985bbea639ba2ea0e7cad5081
This commit is contained in:
Tim Yung
2018-06-11 18:20:52 -07:00
committed by Facebook Github Bot
parent f8b4850425
commit d0219a0301
25 changed files with 2306 additions and 558 deletions

View File

@@ -0,0 +1,146 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
const React = require('React');
const Text = require('Text');
const UTFSequence = require('UTFSequence');
const stringifySafe = require('stringifySafe');
import type {TextStyleProp} from 'StyleSheet';
export type Category = string;
export type Message = $ReadOnly<{|
content: string,
substitutions: $ReadOnlyArray<
$ReadOnly<{|
length: number,
offset: number,
|}>,
>,
|}>;
const SUBSTITUTION = UTFSequence.BOM + '%s';
const YellowBoxCategory = {
parse(
args: $ReadOnlyArray<mixed>,
): $ReadOnly<{|
category: Category,
message: Message,
|}> {
const categoryParts = [];
const contentParts = [];
const substitutionOffsets = [];
const remaining = [...args];
if (typeof remaining[0] === 'string') {
const formatString = String(remaining.shift());
const formatStringParts = formatString.split('%s');
const substitutionCount = formatStringParts.length - 1;
const substitutions = remaining.splice(0, substitutionCount);
let categoryString = '';
let contentString = '';
let substitutionIndex = 0;
for (const formatStringPart of formatStringParts) {
categoryString += formatStringPart;
contentString += formatStringPart;
if (substitutionIndex < substitutionCount) {
if (substitutionIndex < substitutions.length) {
const substitution = stringifySafe(
substitutions[substitutionIndex],
);
substitutionOffsets.push({
length: substitution.length,
offset: contentString.length,
});
categoryString += SUBSTITUTION;
contentString += substitution;
} else {
substitutionOffsets.push({
length: 2,
offset: contentString.length,
});
categoryString += '%s';
contentString += '%s';
}
substitutionIndex++;
}
}
categoryParts.push(categoryString);
contentParts.push(contentString);
}
const remainingArgs = remaining.map(stringifySafe);
categoryParts.push(...remainingArgs);
contentParts.push(...remainingArgs);
return {
category: categoryParts.join(' '),
message: {
content: contentParts.join(' '),
substitutions: substitutionOffsets,
},
};
},
render(
{content, substitutions}: Message,
substitutionStyle: TextStyleProp,
): React.Node {
const elements = [];
const lastOffset = substitutions.reduce(
(prevOffset, substitution, index) => {
const key = String(index);
if (substitution.offset > prevOffset) {
const prevPart = content.substr(
prevOffset,
substitution.offset - prevOffset,
);
elements.push(<Text key={key}>{prevPart}</Text>);
}
const substititionPart = content.substr(
substitution.offset,
substitution.length,
);
elements.push(
<Text key={key + '.5'} style={substitutionStyle}>
{substititionPart}
</Text>,
);
return substitution.offset + substitution.length;
},
0,
);
if (lastOffset < content.length - 1) {
const lastPart = content.substr(lastOffset);
elements.push(<Text key="-1">{lastPart}</Text>);
}
return elements;
},
};
module.exports = YellowBoxCategory;

View File

@@ -0,0 +1,141 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
const YellowBoxWarning = require('YellowBoxWarning');
import type {Category} from 'YellowBoxCategory';
export type Registry = Map<Category, $ReadOnlyArray<YellowBoxWarning>>;
export type Observer = (registry: Registry) => void;
export type Subscription = $ReadOnly<{|
unsubscribe: () => void,
|}>;
const observers: Set<{observer: Observer}> = new Set();
const ignorePatterns: Set<string> = new Set();
const registry: Registry = new Map();
let disabled = false;
let projection = new Map();
let updateTimeout = null;
function isWarningIgnored(warning: YellowBoxWarning): boolean {
for (const pattern of ignorePatterns) {
if (warning.message.content.includes(pattern)) {
return true;
}
}
return false;
}
function handleUpdate(): void {
projection = new Map();
if (!disabled) {
for (const [category, warnings] of registry) {
const filtered = warnings.filter(warning => !isWarningIgnored(warning));
if (filtered.length > 0) {
projection.set(category, filtered);
}
}
}
if (updateTimeout == null) {
updateTimeout = setImmediate(() => {
updateTimeout = null;
for (const {observer} of observers) {
observer(projection);
}
});
}
}
const YellowBoxRegistry = {
add({
args,
framesToPop,
}: $ReadOnly<{|
args: $ReadOnlyArray<mixed>,
framesToPop: number,
|}>): void {
if (typeof args[0] === 'string' && args[0].startsWith('(ADVICE)')) {
return;
}
const {category, message, stack} = YellowBoxWarning.parse({
args,
framesToPop: framesToPop + 1,
});
let warnings = registry.get(category);
if (warnings == null) {
warnings = [];
}
warnings = [...warnings, new YellowBoxWarning(message, stack)];
registry.delete(category);
registry.set(category, warnings);
handleUpdate();
},
delete(category: Category): void {
if (registry.has(category)) {
registry.delete(category);
handleUpdate();
}
},
clear(): void {
if (registry.size > 0) {
registry.clear();
handleUpdate();
}
},
addIgnorePatterns(patterns: $ReadOnlyArray<string>): void {
const newPatterns = patterns.filter(
pattern => !ignorePatterns.has(pattern),
);
if (newPatterns.length === 0) {
return;
}
for (const pattern of newPatterns) {
ignorePatterns.add(pattern);
}
handleUpdate();
},
setDisabled(value: boolean): void {
if (value === disabled) {
return;
}
disabled = value;
handleUpdate();
},
isDisabled(): boolean {
return disabled;
},
observe(observer: Observer): Subscription {
const subscription = {observer};
observers.add(subscription);
observer(projection);
return {
unsubscribe(): void {
observers.delete(subscription);
},
};
},
};
module.exports = YellowBoxRegistry;

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
const symbolicateStackTrace = require('symbolicateStackTrace');
import type {StackFrame} from 'parseErrorStack';
type CacheKey = string;
export type Stack = Array<StackFrame>;
const cache: Map<CacheKey, Promise<Stack>> = new Map();
const YellowBoxSymbolication = {
symbolicate(stack: Stack): Promise<Stack> {
const key = getCacheKey(stack);
let promise = cache.get(key);
if (promise == null) {
promise = symbolicateStackTrace(stack).then(sanitize);
cache.set(key, promise);
}
return promise;
},
};
const getCacheKey = (stack: Stack): CacheKey => {
return JSON.stringify(stack);
};
/**
* Sanitize because sometimes, `symbolicateStackTrace` gives us invalid values.
*/
const sanitize = (maybeStack: mixed): Stack => {
if (!Array.isArray(maybeStack)) {
throw new Error('Expected stack to be an array.');
}
const stack = [];
for (const maybeFrame of maybeStack) {
if (typeof maybeFrame !== 'object' || maybeFrame == null) {
throw new Error('Expected each stack frame to be an object.');
}
if (typeof maybeFrame.column !== 'number' && maybeFrame.column != null) {
throw new Error('Expected stack frame `column` to be a nullable number.');
}
if (typeof maybeFrame.file !== 'string') {
throw new Error('Expected stack frame `file` to be a string.');
}
if (typeof maybeFrame.lineNumber !== 'number') {
throw new Error('Expected stack frame `lineNumber` to be a number.');
}
if (typeof maybeFrame.methodName !== 'string') {
throw new Error('Expected stack frame `methodName` to be a string.');
}
stack.push({
column: maybeFrame.column,
file: maybeFrame.file,
lineNumber: maybeFrame.lineNumber,
methodName: maybeFrame.methodName,
});
}
return stack;
};
module.exports = YellowBoxSymbolication;

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const YellowBoxCategory = require('YellowBoxCategory');
const YellowBoxSymbolication = require('YellowBoxSymbolication');
const parseErrorStack = require('parseErrorStack');
import type {Category, Message} from 'YellowBoxCategory';
import type {Stack} from 'YellowBoxSymbolication';
export type SymbolicationRequest = $ReadOnly<{|
abort: () => void,
|}>;
class YellowBoxWarning {
static parse({
args,
framesToPop,
}: $ReadOnly<{|
args: $ReadOnlyArray<mixed>,
framesToPop: number,
|}>): {|
category: Category,
message: Message,
stack: Stack,
|} {
return {
...YellowBoxCategory.parse(args),
stack: createStack({framesToPop: framesToPop + 1}),
};
}
message: Message;
stack: Stack;
symbolicated:
| $ReadOnly<{|error: null, stack: null, status: 'NONE'|}>
| $ReadOnly<{|error: null, stack: null, status: 'PENDING'|}>
| $ReadOnly<{|error: null, stack: Stack, status: 'COMPLETE'|}>
| $ReadOnly<{|error: Error, stack: null, status: 'FAILED'|}> = {
error: null,
stack: null,
status: 'NONE',
};
constructor(message: Message, stack: Stack) {
this.message = message;
this.stack = stack;
}
getAvailableStack(): Stack {
return this.symbolicated.status === 'COMPLETE'
? this.symbolicated.stack
: this.stack;
}
symbolicate(callback: () => void): SymbolicationRequest {
let aborted = false;
if (this.symbolicated.status !== 'COMPLETE') {
const updateStatus = (error: ?Error, stack: ?Stack): void => {
if (error != null) {
this.symbolicated = {error, stack: null, status: 'FAILED'};
} else if (stack != null) {
this.symbolicated = {error: null, stack, status: 'COMPLETE'};
} else {
this.symbolicated = {error: null, stack: null, status: 'PENDING'};
}
if (!aborted) {
callback();
}
};
updateStatus(null, null);
YellowBoxSymbolication.symbolicate(this.stack).then(
stack => {
updateStatus(null, stack);
},
error => {
updateStatus(error, null);
},
);
}
return {
abort(): void {
aborted = true;
},
};
}
}
function createStack({framesToPop}: $ReadOnly<{|framesToPop: number|}>): Stack {
const error: any = new Error();
error.framesToPop = framesToPop + 1;
return parseErrorStack(error);
}
module.exports = YellowBoxWarning;

View File

@@ -0,0 +1,100 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+react_native
* @format
* @flow strict-local
*/
'use strict';
const YellowBoxCategory = require('YellowBoxCategory');
describe('YellowBoxCategory', () => {
it('parses strings', () => {
expect(YellowBoxCategory.parse(['A'])).toEqual({
category: 'A',
message: {
content: 'A',
substitutions: [],
},
});
});
it('parses strings with arguments', () => {
expect(YellowBoxCategory.parse(['A', 'B', 'C'])).toEqual({
category: 'A "B" "C"',
message: {
content: 'A "B" "C"',
substitutions: [],
},
});
});
it('parses formatted strings', () => {
expect(YellowBoxCategory.parse(['%s', 'A'])).toEqual({
category: '\ufeff%s',
message: {
content: '"A"',
substitutions: [
{
length: 3,
offset: 0,
},
],
},
});
});
it('parses formatted strings with insufficient arguments', () => {
expect(YellowBoxCategory.parse(['%s %s', 'A'])).toEqual({
category: '\ufeff%s %s',
message: {
content: '"A" %s',
substitutions: [
{
length: 3,
offset: 0,
},
{
length: 2,
offset: 4,
},
],
},
});
});
it('parses formatted strings with excess arguments', () => {
expect(YellowBoxCategory.parse(['%s', 'A', 'B'])).toEqual({
category: '\ufeff%s "B"',
message: {
content: '"A" "B"',
substitutions: [
{
length: 3,
offset: 0,
},
],
},
});
});
it('treats "%s" in arguments as literals', () => {
expect(YellowBoxCategory.parse(['%s', '%s', 'A'])).toEqual({
category: '\ufeff%s "A"',
message: {
content: '"%s" "A"',
substitutions: [
{
length: 4,
offset: 0,
},
],
},
});
});
});

View File

@@ -0,0 +1,267 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+react_native
* @format
* @flow strict-local
*/
'use strict';
const YellowBoxCategory = require('YellowBoxCategory');
const YellowBoxRegistry = require('YellowBoxRegistry');
const registry = () => {
const observer = jest.fn();
YellowBoxRegistry.observe(observer).unsubscribe();
return observer.mock.calls[0][0];
};
const observe = () => {
const observer = jest.fn();
return {
observer,
subscription: YellowBoxRegistry.observe(observer),
};
};
describe('YellowBoxRegistry', () => {
beforeEach(() => {
jest.resetModules();
});
it('adds and deletes warnings', () => {
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
const {category: categoryA} = YellowBoxCategory.parse(['A']);
expect(registry().size).toBe(1);
expect(registry().get(categoryA)).not.toBe(undefined);
YellowBoxRegistry.delete(categoryA);
expect(registry().size).toBe(0);
expect(registry().get(categoryA)).toBe(undefined);
});
it('clears all warnings', () => {
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
YellowBoxRegistry.add({args: ['B'], framesToPop: 0});
YellowBoxRegistry.add({args: ['C'], framesToPop: 0});
expect(registry().size).toBe(3);
YellowBoxRegistry.clear();
expect(registry().size).toBe(0);
});
it('sorts warnings in chronological order', () => {
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
YellowBoxRegistry.add({args: ['B'], framesToPop: 0});
YellowBoxRegistry.add({args: ['C'], framesToPop: 0});
const {category: categoryA} = YellowBoxCategory.parse(['A']);
const {category: categoryB} = YellowBoxCategory.parse(['B']);
const {category: categoryC} = YellowBoxCategory.parse(['C']);
expect(Array.from(registry().keys())).toEqual([
categoryA,
categoryB,
categoryC,
]);
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
// Expect `A` to be hoisted to the end of the registry.
expect(Array.from(registry().keys())).toEqual([
categoryB,
categoryC,
categoryA,
]);
});
it('ignores warnings matching patterns', () => {
YellowBoxRegistry.add({args: ['A!'], framesToPop: 0});
YellowBoxRegistry.add({args: ['B?'], framesToPop: 0});
YellowBoxRegistry.add({args: ['C!'], framesToPop: 0});
expect(registry().size).toBe(3);
YellowBoxRegistry.addIgnorePatterns(['!']);
expect(registry().size).toBe(1);
YellowBoxRegistry.addIgnorePatterns(['?']);
expect(registry().size).toBe(0);
});
it('ignores all warnings when disabled', () => {
YellowBoxRegistry.add({args: ['A!'], framesToPop: 0});
YellowBoxRegistry.add({args: ['B?'], framesToPop: 0});
YellowBoxRegistry.add({args: ['C!'], framesToPop: 0});
expect(registry().size).toBe(3);
YellowBoxRegistry.setDisabled(true);
expect(registry().size).toBe(0);
YellowBoxRegistry.setDisabled(false);
expect(registry().size).toBe(3);
});
it('groups warnings by simple categories', () => {
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
expect(registry().size).toBe(1);
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
expect(registry().size).toBe(1);
YellowBoxRegistry.add({args: ['B'], framesToPop: 0});
expect(registry().size).toBe(2);
});
it('groups warnings by format string categories', () => {
YellowBoxRegistry.add({args: ['%s', 'A'], framesToPop: 0});
expect(registry().size).toBe(1);
YellowBoxRegistry.add({args: ['%s', 'B'], framesToPop: 0});
expect(registry().size).toBe(1);
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
expect(registry().size).toBe(2);
YellowBoxRegistry.add({args: ['B'], framesToPop: 0});
expect(registry().size).toBe(3);
});
it('groups warnings with consideration for arguments', () => {
YellowBoxRegistry.add({args: ['A', 'B'], framesToPop: 0});
expect(registry().size).toBe(1);
YellowBoxRegistry.add({args: ['A', 'B'], framesToPop: 0});
expect(registry().size).toBe(1);
YellowBoxRegistry.add({args: ['A', 'C'], framesToPop: 0});
expect(registry().size).toBe(2);
YellowBoxRegistry.add({args: ['%s', 'A', 'A'], framesToPop: 0});
expect(registry().size).toBe(3);
YellowBoxRegistry.add({args: ['%s', 'B', 'A'], framesToPop: 0});
expect(registry().size).toBe(3);
YellowBoxRegistry.add({args: ['%s', 'B', 'B'], framesToPop: 0});
expect(registry().size).toBe(4);
});
it('ignores warnings starting with "(ADVICE)"', () => {
YellowBoxRegistry.add({args: ['(ADVICE) ...'], framesToPop: 0});
expect(registry().size).toBe(0);
});
it('does not ignore warnings formatted to start with "(ADVICE)"', () => {
YellowBoxRegistry.add({args: ['%s ...', '(ADVICE)'], framesToPop: 0});
expect(registry().size).toBe(1);
});
it('immediately updates new observers', () => {
const {observer} = observe();
expect(observer.mock.calls.length).toBe(1);
expect(observer.mock.calls[0][0]).toBe(registry());
});
it('sends batched updates asynchoronously', () => {
const {observer} = observe();
expect(observer.mock.calls.length).toBe(1);
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
YellowBoxRegistry.add({args: ['B'], framesToPop: 0});
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(2);
});
it('stops sending updates to unsubscribed observers', () => {
const {observer, subscription} = observe();
subscription.unsubscribe();
expect(observer.mock.calls.length).toBe(1);
expect(observer.mock.calls[0][0]).toBe(registry());
});
it('updates observers when a warning is added or deleted', () => {
const {observer} = observe();
expect(observer.mock.calls.length).toBe(1);
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(2);
const {category: categoryA} = YellowBoxCategory.parse(['A']);
YellowBoxRegistry.delete(categoryA);
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(3);
// Does nothing when category does not exist.
YellowBoxRegistry.delete(categoryA);
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(3);
});
it('updates observers when cleared', () => {
const {observer} = observe();
expect(observer.mock.calls.length).toBe(1);
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(2);
YellowBoxRegistry.clear();
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(3);
// Does nothing when already empty.
YellowBoxRegistry.clear();
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(3);
});
it('updates observers when an ignore pattern is added', () => {
const {observer} = observe();
expect(observer.mock.calls.length).toBe(1);
YellowBoxRegistry.addIgnorePatterns(['?']);
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(2);
YellowBoxRegistry.addIgnorePatterns(['!']);
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(3);
// Does nothing for an existing ignore pattern.
YellowBoxRegistry.addIgnorePatterns(['!']);
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(3);
});
it('updates observers when disabled or enabled', () => {
const {observer} = observe();
expect(observer.mock.calls.length).toBe(1);
YellowBoxRegistry.setDisabled(true);
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(2);
// Does nothing when already disabled.
YellowBoxRegistry.setDisabled(true);
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(2);
YellowBoxRegistry.setDisabled(false);
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(3);
// Does nothing when already enabled.
YellowBoxRegistry.setDisabled(false);
jest.runAllImmediates();
expect(observer.mock.calls.length).toBe(3);
});
});

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+react_native
* @format
* @flow
*/
'use strict';
import type {StackFrame} from 'parseErrorStack';
jest.mock('symbolicateStackTrace');
const YellowBoxSymbolication = require('YellowBoxSymbolication');
const symbolicateStackTrace: JestMockFn<
$ReadOnlyArray<Array<StackFrame>>,
Promise<Array<StackFrame>>,
> = (require('symbolicateStackTrace'): any);
const createStack = methodNames =>
methodNames.map(methodName => ({
column: null,
file: 'file://path/to/file.js',
lineNumber: 1,
methodName,
}));
describe('YellowBoxSymbolication', () => {
beforeEach(() => {
jest.resetModules();
symbolicateStackTrace.mockImplementation(async stack => stack);
});
it('symbolicates different stacks', () => {
YellowBoxSymbolication.symbolicate(createStack(['A', 'B', 'C']));
YellowBoxSymbolication.symbolicate(createStack(['D', 'E', 'F']));
expect(symbolicateStackTrace.mock.calls.length).toBe(2);
});
it('batch symbolicates equivalent stacks', () => {
YellowBoxSymbolication.symbolicate(createStack(['A', 'B', 'C']));
YellowBoxSymbolication.symbolicate(createStack(['A', 'B', 'C']));
expect(symbolicateStackTrace.mock.calls.length).toBe(1);
});
});

View File

@@ -0,0 +1,126 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+react_native
* @format
* @flow
*/
'use strict';
import type {StackFrame} from 'parseErrorStack';
jest.mock('YellowBoxSymbolication');
const YellowBoxSymbolication: {|
symbolicate: JestMockFn<
$ReadOnlyArray<Array<StackFrame>>,
Promise<Array<StackFrame>>,
>,
|} = (require('YellowBoxSymbolication'): any);
const YellowBoxWarning = require('YellowBoxWarning');
const createStack = methodNames =>
methodNames.map(methodName => ({
column: null,
file: 'file://path/to/file.js',
lineNumber: 1,
methodName,
}));
describe('YellowBoxWarning', () => {
beforeEach(() => {
jest.resetModules();
YellowBoxSymbolication.symbolicate.mockImplementation(async stack =>
createStack(stack.map(frame => `S(${frame.methodName})`)),
);
});
it('starts without a symbolicated stack', () => {
const warning = new YellowBoxWarning(
{content: '...', substitutions: []},
createStack(['A', 'B', 'C']),
);
expect(warning.symbolicated).toEqual({
error: null,
stack: null,
status: 'NONE',
});
});
it('updates when symbolication is in progress', () => {
const warning = new YellowBoxWarning(
{content: '...', substitutions: []},
createStack(['A', 'B', 'C']),
);
const callback = jest.fn();
warning.symbolicate(callback);
expect(callback.mock.calls.length).toBe(1);
expect(warning.symbolicated).toEqual({
error: null,
stack: null,
status: 'PENDING',
});
});
it('updates when symbolication finishes', () => {
const warning = new YellowBoxWarning(
{content: '...', substitutions: []},
createStack(['A', 'B', 'C']),
);
const callback = jest.fn();
warning.symbolicate(callback);
jest.runAllTicks();
expect(callback.mock.calls.length).toBe(2);
expect(warning.symbolicated).toEqual({
error: null,
stack: createStack(['S(A)', 'S(B)', 'S(C)']),
status: 'COMPLETE',
});
});
it('updates when symbolication fails', () => {
const error = new Error('...');
YellowBoxSymbolication.symbolicate.mockImplementation(async stack => {
throw error;
});
const warning = new YellowBoxWarning(
{content: '...', substitutions: []},
createStack(['A', 'B', 'C']),
);
const callback = jest.fn();
warning.symbolicate(callback);
jest.runAllTicks();
expect(callback.mock.calls.length).toBe(2);
expect(warning.symbolicated).toEqual({
error,
stack: null,
status: 'FAILED',
});
});
it('does not update aborted requests', () => {
const warning = new YellowBoxWarning(
{content: '...', substitutions: []},
createStack(['A', 'B', 'C']),
);
const callback = jest.fn();
const request = warning.symbolicate(callback);
request.abort();
jest.runAllTicks();
expect(callback.mock.calls.length).toBe(1);
});
});