mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-24 04:16:00 +08:00
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:
committed by
Facebook Github Bot
parent
f8b4850425
commit
d0219a0301
146
Libraries/YellowBox/Data/YellowBoxCategory.js
Normal file
146
Libraries/YellowBox/Data/YellowBoxCategory.js
Normal 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;
|
||||
141
Libraries/YellowBox/Data/YellowBoxRegistry.js
Normal file
141
Libraries/YellowBox/Data/YellowBoxRegistry.js
Normal 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;
|
||||
75
Libraries/YellowBox/Data/YellowBoxSymbolication.js
Normal file
75
Libraries/YellowBox/Data/YellowBoxSymbolication.js
Normal 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;
|
||||
108
Libraries/YellowBox/Data/YellowBoxWarning.js
Normal file
108
Libraries/YellowBox/Data/YellowBoxWarning.js
Normal 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;
|
||||
100
Libraries/YellowBox/Data/__tests__/YellowBoxCategory-test.js
Normal file
100
Libraries/YellowBox/Data/__tests__/YellowBoxCategory-test.js
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
267
Libraries/YellowBox/Data/__tests__/YellowBoxRegistry-test.js
Normal file
267
Libraries/YellowBox/Data/__tests__/YellowBoxRegistry-test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
126
Libraries/YellowBox/Data/__tests__/YellowBoxWarning-test.js
Normal file
126
Libraries/YellowBox/Data/__tests__/YellowBoxWarning-test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user