feat(trace): convert to TypeScript (#647)

* get started

* progress

* finish test

* Revert that file

* Move types to dev

* Add enzyme types

* Bump that version

* prettier
This commit is contained in:
Nathan Bierema 2020-09-29 09:37:13 -04:00 committed by GitHub
parent d37e7d93e1
commit 03217001df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 434 additions and 321 deletions

View File

@ -84,6 +84,7 @@
"peerDependencies": { "peerDependencies": {
"@types/react": "^16.3.18", "@types/react": "^16.3.18",
"react": "^16.3.0", "react": "^16.3.0",
"redux": "^3.4.0 || ^4.0.0",
"redux-devtools-inspector-monitor": "^0.14.0" "redux-devtools-inspector-monitor": "^0.14.0"
} }
} }

View File

@ -1,13 +1,8 @@
{ {
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"], "presets": [
"plugins": [ "@babel/preset-env",
[ "@babel/preset-react",
"@babel/plugin-transform-runtime", "@babel/preset-typescript"
{ ],
"regenerator": true "plugins": ["@babel/plugin-proposal-class-properties"]
}
],
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-class-properties"
]
} }

View File

@ -0,0 +1 @@
lib

View File

@ -0,0 +1,21 @@
module.exports = {
extends: '../../.eslintrc',
overrides: [
{
files: ['*.ts', '*.tsx'],
extends: '../../eslintrc.ts.react.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
},
{
files: ['test/*.ts', 'test/*.tsx'],
extends: '../../eslintrc.ts.react.jest.base.json',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./test/tsconfig.json'],
},
},
],
};

View File

@ -0,0 +1,3 @@
module.exports = {
preset: 'ts-jest',
};

View File

@ -2,47 +2,61 @@
"name": "redux-devtools-trace-monitor", "name": "redux-devtools-trace-monitor",
"version": "0.1.3", "version": "0.1.3",
"description": "Submonitor for Redux DevTools inspector to show stack traces.", "description": "Submonitor for Redux DevTools inspector to show stack traces.",
"repository": "https://github.com/reduxjs/redux-devtools", "homepage": "https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-trace-monitor",
"homepage": "https://github.com/reduxjs/redux-devtools", "license": "MIT",
"author": "Mark Erikson <mark@isquaredsoftware.com>", "author": "Mark Erikson <mark@isquaredsoftware.com>",
"contributors": [ "contributors": [
"Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)" "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)"
], ],
"license": "MIT",
"main": "lib/StackTraceTab.js",
"files": [ "files": [
"lib" "lib"
], ],
"main": "lib/StackTraceTab.js",
"types": "lib/StackTraceTab.d.ts",
"repository": "https://github.com/reduxjs/redux-devtools",
"scripts": { "scripts": {
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
"clean": "rimraf lib", "clean": "rimraf lib",
"build": "babel src --out-dir lib", "test": "jest",
"test": "jest --no-cache", "lint": "eslint . --ext .ts,.tsx",
"prepare": "npm run clean && npm run build", "lint:fix": "eslint . --ext .ts,.tsx --fix",
"prepublishOnly": "npm run test && npm run clean && npm run build" "type-check": "tsc --noEmit",
}, "type-check:watch": "npm run type-check -- --watch",
"devDependencies": { "preversion": "npm run type-check && npm run lint && npm run test",
"@babel/cli": "^7.10.5", "prepublishOnly": "npm run clean && npm run build"
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-transform-runtime": "^7.11.0",
"@babel/preset-env": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
"enzyme-to-json": "^3.5.0",
"jest": "^26.2.2",
"react-dom": "^16.13.1",
"react-test-renderer": "^16.13.1",
"rimraf": "^3.0.2"
}, },
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.10.4",
"@types/chrome": "^0.0.124",
"anser": "^1.4.9", "anser": "^1.4.9",
"html-entities": "^1.3.1", "html-entities": "^1.3.1",
"react": "^16.13.1",
"redux-devtools-themes": "^1.0.0", "redux-devtools-themes": "^1.0.0",
"settle-promise": "^1.0.0" "settle-promise": "^1.0.0"
},
"devDependencies": {
"@types/babel__code-frame": "^7.0.2",
"@types/enzyme": "^3.10.5",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/html-entities": "^1.2.16",
"@types/react": "^16.9.46",
"@types/redux-devtools-themes": "^1.0.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.3",
"enzyme-to-json": "^3.5.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-test-renderer": "^16.13.1",
"redux": "^4.0.5",
"redux-devtools": "^3.7.0",
"redux-devtools-inspector-monitor": "^0.14.0"
},
"peerDependencies": {
"@types/react": "^16.3.18",
"react": "^16.3.0",
"redux": "^3.4.0 || ^4.0.0",
"redux-devtools": "^3.4.0",
"redux-devtools-inspector-monitor": "^0.14.0"
} }
} }

View File

@ -3,26 +3,45 @@ import React, { Component } from 'react';
import { getStackFrames } from './react-error-overlay/utils/getStackFrames'; import { getStackFrames } from './react-error-overlay/utils/getStackFrames';
import StackTrace from './react-error-overlay/containers/StackTrace'; import StackTrace from './react-error-overlay/containers/StackTrace';
import openFile from './openFile'; import openFile from './openFile';
import { Action } from 'redux';
import { TabComponentProps } from 'redux-devtools-inspector-monitor';
import StackFrame from './react-error-overlay/utils/stack-frame';
import { ErrorLocation } from './react-error-overlay/utils/parseCompileError';
const rootStyle = { padding: '5px 10px' }; const rootStyle = { padding: '5px 10px' };
export default class StackTraceTab extends Component { interface Props<S, A extends Action<unknown>> extends TabComponentProps<S, A> {
openFile: (
fileName: string,
lineNumber: number,
stackFrame: StackFrame
) => void;
}
interface State {
stackFrames: StackFrame[];
currentError?: Error;
showDocsLink?: boolean;
}
export default class StackTraceTab<
S,
A extends Action<unknown>
> extends Component<Props<S, A>, State> {
static defaultProps = { static defaultProps = {
openFile, openFile,
}; };
constructor(props) {
super(props);
this.state = { state: State = {
stackFrames: [], stackFrames: [],
}; };
}
componentDidMount() { componentDidMount() {
// console.log("StackTraceTab mounted"); // console.log("StackTraceTab mounted");
this.checkForStackTrace(); this.checkForStackTrace();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: Props<S, A>) {
const { action, actions } = prevProps; const { action, actions } = prevProps;
if (action !== this.props.action || actions !== this.props.actions) { if (action !== this.props.action || actions !== this.props.actions) {
@ -47,25 +66,32 @@ export default class StackTraceTab extends Component {
stack: liftedAction.stack, stack: liftedAction.stack,
}); });
getStackFrames(deserializedError).then((stackFrames) => { getStackFrames(deserializedError)
/* eslint-disable no-console */ .then((stackFrames) => {
if (process.env.NODE_ENV === 'development') /* eslint-disable no-console */
console.log('Stack frames: ', stackFrames); if (process.env.NODE_ENV === 'development')
/* eslint-enable no-console */ console.log('Stack frames: ', stackFrames);
this.setState({ stackFrames, currentError: deserializedError }); /* eslint-enable no-console */
}); this.setState({
stackFrames: stackFrames!,
currentError: deserializedError,
});
})
.catch(() => {
// noop
});
} else { } else {
this.setState({ this.setState({
stackFrames: [], stackFrames: [],
showDocsLink: showDocsLink:
liftedAction.action && liftedAction!.action &&
liftedAction.action.type && liftedAction!.action.type &&
liftedAction.action.type !== '@@INIT', liftedAction!.action.type !== '@@INIT',
}); });
} }
} }
onStackLocationClicked = (fileLocation = {}) => { onStackLocationClicked = (fileLocation: Partial<ErrorLocation> = {}) => {
// console.log("Stack location args: ", ...args); // console.log("Stack location args: ", ...args);
const { fileName, lineNumber } = fileLocation; const { fileName, lineNumber } = fileLocation;
@ -93,7 +119,7 @@ export default class StackTraceTab extends Component {
} }
}; };
openDocs = (e) => { openDocs: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
e.stopPropagation(); e.stopPropagation();
window.open( window.open(
'https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/Features/Trace.md' 'https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/Features/Trace.md'

View File

@ -1,31 +1,35 @@
import StackFrame from './react-error-overlay/utils/stack-frame';
const isFF = navigator.userAgent.indexOf('Firefox') !== -1; const isFF = navigator.userAgent.indexOf('Firefox') !== -1;
function openResource(fileName, lineNumber, stackFrame) { function openResource(
fileName: string,
lineNumber: number,
stackFrame: StackFrame
) {
const adjustedLineNumber = Math.max(lineNumber - 1, 0); const adjustedLineNumber = Math.max(lineNumber - 1, 0);
chrome.devtools.panels.openResource( chrome.devtools.panels.openResource(fileName, adjustedLineNumber, ((result: {
fileName, isError?: boolean;
adjustedLineNumber, }) => {
(result) => { //console.log("openResource callback args: ", callbackArgs);
//console.log("openResource callback args: ", callbackArgs); if (result.isError) {
if (result.isError) { const {
const { fileName: finalFileName,
fileName: finalFileName, lineNumber: finalLineNumber,
lineNumber: finalLineNumber, } = stackFrame;
} = stackFrame; const adjustedLineNumber = Math.max(finalLineNumber! - 1, 0);
const adjustedLineNumber = Math.max(finalLineNumber - 1, 0); chrome.devtools.panels.openResource(
chrome.devtools.panels.openResource( finalFileName!,
finalFileName, adjustedLineNumber,
adjustedLineNumber, (/* result */) => {
(/* result */) => { // console.log("openResource result: ", result);
// console.log("openResource result: ", result); }
} );
);
}
} }
); }) as () => void);
} }
function openAndCloseTab(url) { function openAndCloseTab(url: string) {
chrome.tabs.create({ url }, (tab) => { chrome.tabs.create({ url }, (tab) => {
const removeTab = () => { const removeTab = () => {
chrome.windows.onFocusChanged.removeListener(removeTab); chrome.windows.onFocusChanged.removeListener(removeTab);
@ -45,19 +49,19 @@ function openAndCloseTab(url) {
}); });
} }
function openInIframe(url) { function openInIframe(url: string) {
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.src = url; iframe.src = url;
iframe.style = 'display:none'; iframe.style.display = 'none';
document.body.appendChild(iframe); document.body.appendChild(iframe);
setTimeout(() => iframe.parentNode.removeChild(iframe), 3000); setTimeout(() => iframe.parentNode!.removeChild(iframe), 3000);
} }
function openInEditor(editor, path, stackFrame) { function openInEditor(editor: string, path: string, stackFrame: StackFrame) {
const projectPath = path.replace(/\/$/, ''); const projectPath = path.replace(/\/$/, '');
const file = const file =
stackFrame._originalFileName || stackFrame._originalFileName ||
stackFrame.finalFileName || ((stackFrame as unknown) as { finalFileName: string }).finalFileName ||
stackFrame.fileName || stackFrame.fileName ||
''; '';
let filePath = /^https?:\/\//.test(file) let filePath = /^https?:\/\//.test(file)
@ -95,7 +99,11 @@ function openInEditor(editor, path, stackFrame) {
} }
} }
export default function openFile(fileName, lineNumber, stackFrame) { export default function openFile(
fileName: string,
lineNumber: number,
stackFrame: StackFrame
) {
if (process.env.NODE_ENV === 'development') if (process.env.NODE_ENV === 'development')
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(fileName, lineNumber, stackFrame); console.log(fileName, lineNumber, stackFrame);

View File

@ -5,10 +5,9 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */ import React, { CSSProperties } from 'react';
import React from 'react';
const preStyle = { const preStyle: CSSProperties = {
position: 'relative', position: 'relative',
display: 'block', display: 'block',
backgroundColor: '#000', backgroundColor: '#000',
@ -24,10 +23,10 @@ const codeStyle = {
fontFamily: 'Consolas, Menlo, monospace', fontFamily: 'Consolas, Menlo, monospace',
}; };
type CodeBlockPropsType = {| interface CodeBlockPropsType {
main: boolean, main: boolean;
codeHTML: string, codeHTML: string;
|}; }
function CodeBlock(props: CodeBlockPropsType) { function CodeBlock(props: CodeBlockPropsType) {
const codeBlock = { __html: props.codeHTML }; const codeBlock = { __html: props.codeHTML };

View File

@ -5,13 +5,10 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */ import React, { Component, CSSProperties, ReactNode } from 'react';
import React, { Component } from 'react';
import { nicinabox as theme } from 'redux-devtools-themes'; import { nicinabox as theme } from 'redux-devtools-themes';
import type { Element as ReactElement } from 'react'; const _collapsibleStyle: CSSProperties = {
const _collapsibleStyle = {
color: theme.base06, color: theme.base06,
backgroundColor: theme.base01, backgroundColor: theme.base01,
cursor: 'pointer', cursor: 'pointer',
@ -24,26 +21,27 @@ const _collapsibleStyle = {
lineHeight: '1.5', lineHeight: '1.5',
}; };
const collapsibleCollapsedStyle = { const collapsibleCollapsedStyle: CSSProperties = {
..._collapsibleStyle, ..._collapsibleStyle,
marginBottom: '1.5em', marginBottom: '1.5em',
}; };
const collapsibleExpandedStyle = { const collapsibleExpandedStyle: CSSProperties = {
..._collapsibleStyle, ..._collapsibleStyle,
marginBottom: '0.6em', marginBottom: '0.6em',
}; };
type Props = {| interface Props {
children: ReactElement<any>[], collapsedByDefault?: boolean;
|}; children: ReactNode[];
}
type State = {| interface State {
collapsed: boolean, collapsed: boolean | undefined;
|}; }
class Collapsible extends Component<Props, State> { class Collapsible extends Component<Props, State> {
state = { state: State = {
collapsed: undefined, collapsed: undefined,
}; };
@ -53,7 +51,7 @@ class Collapsible extends Component<Props, State> {
})); }));
}; };
isCollapsed = (state) => isCollapsed = (state: State) =>
state.collapsed === undefined state.collapsed === undefined
? this.props.collapsedByDefault ? this.props.collapsedByDefault
: state.collapsed; : state.collapsed;

View File

@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */ import React, { Component, CSSProperties } from 'react';
import React, { Component } from 'react';
import CodeBlock from './StackFrameCodeBlock'; import CodeBlock from './StackFrameCodeBlock';
import { getPrettyURL } from '../utils/getPrettyURL'; import { getPrettyURL } from '../utils/getPrettyURL';
import { nicinabox as theme } from 'redux-devtools-themes'; import { nicinabox as theme } from 'redux-devtools-themes';
@ -14,22 +13,22 @@ import { nicinabox as theme } from 'redux-devtools-themes';
import type { StackFrame as StackFrameType } from '../utils/stack-frame'; import type { StackFrame as StackFrameType } from '../utils/stack-frame';
import type { ErrorLocation } from '../utils/parseCompileError'; import type { ErrorLocation } from '../utils/parseCompileError';
const linkStyle = { const linkStyle: CSSProperties = {
fontSize: '0.9em', fontSize: '0.9em',
marginBottom: '0.9em', marginBottom: '0.9em',
}; };
const anchorStyle = { const anchorStyle: CSSProperties = {
textDecoration: 'none', textDecoration: 'none',
color: theme.base05, color: theme.base05,
cursor: 'pointer', cursor: 'pointer',
}; };
const codeAnchorStyle = { const codeAnchorStyle: CSSProperties = {
cursor: 'pointer', cursor: 'pointer',
}; };
const toggleStyle = { const toggleStyle: CSSProperties = {
marginBottom: '1.5em', marginBottom: '1.5em',
color: theme.base05, color: theme.base05,
cursor: 'pointer', cursor: 'pointer',
@ -44,20 +43,20 @@ const toggleStyle = {
lineHeight: '1.5', lineHeight: '1.5',
}; };
type Props = {| interface Props {
frame: StackFrameType, frame: StackFrameType;
contextSize: number, contextSize: number;
critical: boolean, critical: boolean;
showCode: boolean, showCode: boolean;
editorHandler: (errorLoc: ErrorLocation) => void, editorHandler: (errorLoc: ErrorLocation) => void;
|}; }
type State = {| interface State {
compiled: boolean, compiled: boolean;
|}; }
class StackFrame extends Component<Props, State> { class StackFrame extends Component<Props, State> {
state = { state: State = {
compiled: false, compiled: false,
}; };
@ -93,7 +92,9 @@ class StackFrame extends Component<Props, State> {
this.props.editorHandler(errorLoc); this.props.editorHandler(errorLoc);
}; };
onKeyDown = (e /* : SyntheticKeyboardEvent<> */) => { onKeyDown: React.KeyboardEventHandler<HTMLSpanElement> = (
e /* : SyntheticKeyboardEvent<> */
) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
this.editorHandler(); this.editorHandler();
} }
@ -162,10 +163,10 @@ class StackFrame extends Component<Props, State> {
<div>{functionName}</div> <div>{functionName}</div>
<div style={linkStyle}> <div style={linkStyle}>
<span <span
style={canOpenInEditor ? anchorStyle : null} style={canOpenInEditor ? anchorStyle : undefined}
onClick={canOpenInEditor ? this.editorHandler : null} onClick={canOpenInEditor ? this.editorHandler : undefined}
onKeyDown={canOpenInEditor ? this.onKeyDown : null} onKeyDown={canOpenInEditor ? this.onKeyDown : undefined}
tabIndex={canOpenInEditor ? '0' : null} tabIndex={canOpenInEditor ? 0 : undefined}
> >
{url} {url}
</span> </span>
@ -173,8 +174,8 @@ class StackFrame extends Component<Props, State> {
{codeBlockProps && ( {codeBlockProps && (
<span> <span>
<span <span
onClick={canOpenInEditor ? this.editorHandler : null} onClick={canOpenInEditor ? this.editorHandler : undefined}
style={canOpenInEditor ? codeAnchorStyle : null} style={canOpenInEditor ? codeAnchorStyle : undefined}
> >
<CodeBlock {...codeBlockProps} /> <CodeBlock {...codeBlockProps} />
</span> </span>

View File

@ -5,40 +5,31 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */
import React from 'react'; import React from 'react';
import CodeBlock from '../components/CodeBlock'; import CodeBlock from '../components/CodeBlock';
import { applyStyles } from '../utils/dom/css'; import { applyStyles } from '../utils/dom/css';
import { absolutifyCaret } from '../utils/dom/absolutifyCaret'; import { absolutifyCaret } from '../utils/dom/absolutifyCaret';
// import type { ScriptLine } from '../utils/stack-frame'; import { ScriptLine } from '../utils/stack-frame';
import generateAnsiHTML from '../utils/generateAnsiHTML'; import generateAnsiHTML from '../utils/generateAnsiHTML';
import { codeFrameColumns } from '@babel/code-frame'; import { codeFrameColumns } from '@babel/code-frame';
import { nicinabox as theme } from 'redux-devtools-themes'; import { nicinabox as theme } from 'redux-devtools-themes';
/* interface StackFrameCodeBlockPropsType {
type StackFrameCodeBlockPropsType = {| lines: ScriptLine[];
lines: ScriptLine[], lineNum: number;
lineNum: number, columnNum: number | null | undefined;
columnNum: ?number, contextSize: number;
contextSize: number, main: boolean;
main: boolean, }
|};
// Exact type workaround for spread operator. function StackFrameCodeBlock(props: StackFrameCodeBlockPropsType) {
// See: https://github.com/facebook/flow/issues/2405
type Exact<T> = $Shape<T>;
*/
function StackFrameCodeBlock(
props /* : Exact<StackFrameCodeBlockPropsType> */
) {
const { lines, lineNum, columnNum, contextSize, main } = props; const { lines, lineNum, columnNum, contextSize, main } = props;
const sourceCode = []; const sourceCode: string[] = [];
let whiteSpace = Infinity; let whiteSpace = Infinity;
lines.forEach(function (e) { lines.forEach(function (e) {
const { content: text } = e; const { content: text } = e;
const m = text.match(/^\s*/); const m = /^\s*/.exec(text);
if (text === '') { if (text === '') {
return; return;
} }
@ -86,16 +77,16 @@ function StackFrameCodeBlock(
const ccn2 = node.childNodes; const ccn2 = node.childNodes;
for (let index2 = 0; index2 < ccn2.length; ++index2) { for (let index2 = 0; index2 < ccn2.length; ++index2) {
const lineNode = ccn2[index2]; const lineNode = ccn2[index2];
const text = lineNode.innerText; const text = (lineNode as HTMLElement).innerText;
if (text == null) { if (text == null) {
continue; continue;
} }
if (text.indexOf(' ' + lineNum + ' |') === -1) { if (text.indexOf(` ${lineNum} |`) === -1) {
continue; continue;
} }
// $FlowFixMe // $FlowFixMe
applyStyles(node, { applyStyles(node as HTMLElement, {
'background-color': main ? theme.base02 : theme.base01, backgroundColor: main ? theme.base02 : theme.base01,
}); });
// eslint-disable-next-line // eslint-disable-next-line
break oLoop; break oLoop;

View File

@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */ import React, { Component, ReactElement } from 'react';
import React, { Component } from 'react';
import StackFrame from './StackFrame'; import StackFrame from './StackFrame';
import Collapsible from '../components/Collapsible'; import Collapsible from '../components/Collapsible';
import { isInternalFile } from '../utils/isInternalFile'; import { isInternalFile } from '../utils/isInternalFile';
@ -22,19 +21,19 @@ const traceStyle = {
overflow: 'auto', overflow: 'auto',
}; };
type Props = {| interface Props {
stackFrames: StackFrameType[], stackFrames: StackFrameType[];
errorName: string, errorName: string;
contextSize: number, contextSize: number;
editorHandler: (errorLoc: ErrorLocation) => void, editorHandler: (errorLoc: ErrorLocation) => void;
|}; }
class StackTrace extends Component<Props> { class StackTrace extends Component<Props> {
renderFrames() { renderFrames() {
const { stackFrames, errorName, contextSize, editorHandler } = this.props; const { stackFrames, errorName, contextSize, editorHandler } = this.props;
const renderedFrames = []; const renderedFrames: ReactElement[] = [];
let hasReachedAppCode = false, let hasReachedAppCode = false,
currentBundle = [], currentBundle: ReactElement[] = [],
bundleCount = 0, bundleCount = 0,
anyNodeExpanded = false; anyNodeExpanded = false;
@ -55,7 +54,7 @@ class StackTrace extends Component<Props> {
const frameEle = ( const frameEle = (
<StackFrame <StackFrame
key={'frame-' + index} key={`frame-${index}`}
frame={frame} frame={frame}
contextSize={contextSize} contextSize={contextSize}
critical={index === 0} critical={index === 0}
@ -77,7 +76,7 @@ class StackTrace extends Component<Props> {
renderedFrames.push( renderedFrames.push(
<Collapsible <Collapsible
collapsedByDefault={anyNodeExpanded} collapsedByDefault={anyNodeExpanded}
key={'bundle-' + bundleCount} key={`bundle-${bundleCount}`}
> >
{currentBundle} {currentBundle}
</Collapsible> </Collapsible>

View File

@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */ function removeNextBr(parent: Node, component: Element | null | undefined) {
function removeNextBr(parent, component: ?Element) {
while (component != null && component.tagName.toLowerCase() !== 'br') { while (component != null && component.tagName.toLowerCase() !== 'br') {
component = component.nextElementSibling; component = component.nextElementSibling;
} }
@ -18,7 +17,7 @@ function removeNextBr(parent, component: ?Element) {
function absolutifyCaret(component: Node) { function absolutifyCaret(component: Node) {
const ccn = component.childNodes; const ccn = component.childNodes;
for (let index = 0; index < ccn.length; ++index) { for (let index = 0; index < ccn.length; ++index) {
const c = ccn[index]; const c = ccn[index] as HTMLElement;
// $FlowFixMe // $FlowFixMe
if (c.tagName.toLowerCase() !== 'span') { if (c.tagName.toLowerCase() !== 'span') {
continue; continue;

View File

@ -5,9 +5,8 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */
let injectedCount = 0; let injectedCount = 0;
const injectedCache = {}; const injectedCache: { [key: number]: HTMLStyleElement } = {};
function getHead(document: Document) { function getHead(document: Document) {
return document.head || document.getElementsByTagName('head')[0]; return document.head || document.getElementsByTagName('head')[0];
@ -33,14 +32,17 @@ function removeCss(document: Document, ref: number) {
delete injectedCache[ref]; delete injectedCache[ref];
} }
function applyStyles(element: HTMLElement, styles: Object) { function applyStyles(
element: HTMLElement,
styles: Partial<CSSStyleDeclaration>
) {
element.setAttribute('style', ''); element.setAttribute('style', '');
for (const key in styles) { for (const key in styles) {
if (!Object.prototype.hasOwnProperty.call(styles, key)) { if (!Object.prototype.hasOwnProperty.call(styles, key)) {
continue; continue;
} }
// $FlowFixMe // $FlowFixMe
element.style[key] = styles[key]; element.style[key] = styles[key]!;
} }
} }

View File

@ -5,15 +5,13 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */
import Anser from 'anser'; import Anser from 'anser';
import { nicinabox as theme } from 'redux-devtools-themes'; import { nicinabox as theme } from 'redux-devtools-themes';
import { AllHtmlEntities as Entities } from 'html-entities'; import { AllHtmlEntities as Entities } from 'html-entities';
var entities = new Entities(); const entities = new Entities();
var anserMap = { const anserMap = {
'ansi-bright-black': theme.base03, 'ansi-bright-black': theme.base03,
'ansi-bright-yellow': theme.base0A, 'ansi-bright-yellow': theme.base0A,
'ansi-yellow': theme.base0B, 'ansi-yellow': theme.base0B,
@ -29,25 +27,25 @@ var anserMap = {
}; };
function generateAnsiHTML(txt: string): string { function generateAnsiHTML(txt: string): string {
var arr = new Anser().ansiToJson(entities.encode(txt), { const arr = new Anser().ansiToJson(entities.encode(txt), {
use_classes: true, use_classes: true,
}); });
var result = ''; let result = '';
var open = false; let open = false;
for (var index = 0; index < arr.length; ++index) { for (let index = 0; index < arr.length; ++index) {
var c = arr[index]; const c = arr[index];
var content = c.content, const content = c.content,
fg = c.fg; fg = c.fg;
var contentParts = content.split('\n'); const contentParts = content.split('\n');
for (var _index = 0; _index < contentParts.length; ++_index) { for (let _index = 0; _index < contentParts.length; ++_index) {
if (!open) { if (!open) {
result += '<span data-ansi-line="true">'; result += '<span data-ansi-line="true">';
open = true; open = true;
} }
var part = contentParts[_index].replace('\r', ''); const part = contentParts[_index].replace('\r', '');
var color = anserMap[fg]; const color = anserMap[fg as keyof typeof anserMap];
if (color != null) { if (color != null) {
result += '<span style="color: ' + color + ';">' + part + '</span>'; result += '<span style="color: ' + color + ';">' + part + '</span>';
} else { } else {

View File

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */
import { ScriptLine } from './stack-frame'; import { ScriptLine } from './stack-frame';
/** /**

View File

@ -5,14 +5,13 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */
function getPrettyURL( function getPrettyURL(
sourceFileName: ?string, sourceFileName: string | null | undefined,
sourceLineNumber: ?number, sourceLineNumber: number | null | undefined,
sourceColumnNumber: ?number, sourceColumnNumber: number | null | undefined,
fileName: ?string, fileName: string | null | undefined,
lineNumber: ?number, lineNumber: number | null | undefined,
columnNumber: ?number, columnNumber: number | null | undefined,
compiled: boolean compiled: boolean
): string { ): string {
let prettyURL; let prettyURL;
@ -26,16 +25,16 @@ function getPrettyURL(
} else { } else {
prettyURL = sourceFileName; prettyURL = sourceFileName;
} }
prettyURL += ':' + sourceLineNumber; prettyURL += `:${sourceLineNumber}`;
// Note: we intentionally skip 0's because they're produced by cheap Webpack maps // Note: we intentionally skip 0's because they're produced by cheap Webpack maps
if (sourceColumnNumber) { if (sourceColumnNumber) {
prettyURL += ':' + sourceColumnNumber; prettyURL += `:${sourceColumnNumber}`;
} }
} else if (fileName && typeof lineNumber === 'number') { } else if (fileName && typeof lineNumber === 'number') {
prettyURL = fileName + ':' + lineNumber; prettyURL = `${fileName}:${lineNumber}`;
// Note: we intentionally skip 0's because they're produced by cheap Webpack maps // Note: we intentionally skip 0's because they're produced by cheap Webpack maps
if (columnNumber) { if (columnNumber) {
prettyURL += ':' + columnNumber; prettyURL += `:${columnNumber}`;
} }
} else { } else {
prettyURL = 'unknown'; prettyURL = 'unknown';

View File

@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */ import { RawSourceMap, SourceMapConsumer } from 'source-map';
import { SourceMapConsumer } from 'source-map';
/** /**
* A wrapped instance of a <code>{@link https://github.com/mozilla/source-map SourceMapConsumer}</code>. * A wrapped instance of a <code>{@link https://github.com/mozilla/source-map SourceMapConsumer}</code>.
@ -16,7 +15,7 @@ import { SourceMapConsumer } from 'source-map';
class SourceMap { class SourceMap {
__source_map: SourceMapConsumer; __source_map: SourceMapConsumer;
constructor(sourceMap) { constructor(sourceMap: SourceMapConsumer) {
this.__source_map = sourceMap; this.__source_map = sourceMap;
} }
@ -28,7 +27,7 @@ class SourceMap {
getOriginalPosition( getOriginalPosition(
line: number, line: number,
column: number column: number
): { source: string, line: number, column: number } { ): { source: string; line: number; column: number } {
const { const {
line: l, line: l,
column: c, column: c,
@ -50,7 +49,7 @@ class SourceMap {
source: string, source: string,
line: number, line: number,
column: number column: number
): { line: number, column: number } { ): { line: number; column: number } {
const { line: l, column: c } = this.__source_map.generatedPositionFor({ const { line: l, column: c } = this.__source_map.generatedPositionFor({
source, source,
line, line,
@ -71,7 +70,7 @@ class SourceMap {
} }
getSources(): string[] { getSources(): string[] {
return this.__source_map.sources; return ((this.__source_map as unknown) as { sources: string[] }).sources;
} }
} }
@ -82,7 +81,7 @@ function extractSourceMapUrl(
const regex = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/gm; const regex = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/gm;
let match = null; let match = null;
for (;;) { for (;;) {
let next = regex.exec(fileContents); const next = regex.exec(fileContents);
if (next == null) { if (next == null) {
break; break;
} }
@ -107,7 +106,7 @@ async function getSourceMap(
let sm = await extractSourceMapUrl(fileUri, fileContents); let sm = await extractSourceMapUrl(fileUri, fileContents);
if (sm.indexOf('data:') === 0) { if (sm.indexOf('data:') === 0) {
const base64 = /^data:application\/json;([\w=:"-]+;)*base64,/; const base64 = /^data:application\/json;([\w=:"-]+;)*base64,/;
const match2 = sm.match(base64); const match2 = base64.exec(sm);
if (!match2) { if (!match2) {
throw new Error( throw new Error(
'Sorry, non-base64 inline source-map encoding is not supported.' 'Sorry, non-base64 inline source-map encoding is not supported.'
@ -116,7 +115,9 @@ async function getSourceMap(
sm = sm.substring(match2[0].length); sm = sm.substring(match2[0].length);
sm = window.atob(sm); sm = window.atob(sm);
sm = JSON.parse(sm); sm = JSON.parse(sm);
return new SourceMap(new SourceMapConsumer(sm)); return new SourceMap(
new SourceMapConsumer((sm as unknown) as RawSourceMap)
);
} else { } else {
const index = fileUri.lastIndexOf('/'); const index = fileUri.lastIndexOf('/');
const url = fileUri.substring(0, index + 1) + sm; const url = fileUri.substring(0, index + 1) + sm;

View File

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */
import type { StackFrame } from './stack-frame'; import type { StackFrame } from './stack-frame';
import { parse } from './parser'; import { parse } from './parser';
import { map } from './mapper'; import { map } from './mapper';
@ -14,15 +13,21 @@ import { toExclude } from '../../presets';
function getStackFrames( function getStackFrames(
error: Error, error: Error,
unhandledRejection: boolean = false, // eslint-disable-line no-unused-vars unhandledRejection = false, // eslint-disable-line no-unused-vars
contextSize: number = 3 contextSize = 3
): Promise<StackFrame[] | null> { ): Promise<StackFrame[] | null> {
const parsedFrames = parse(error); const parsedFrames = parse(error);
let enhancedFramesPromise; let enhancedFramesPromise;
if (error.__unmap_source) { if (
((error as unknown) as {
__unmap_source: string | { uri: string; contents: string };
}).__unmap_source
) {
enhancedFramesPromise = unmap( enhancedFramesPromise = unmap(
// $FlowFixMe // $FlowFixMe
error.__unmap_source, ((error as unknown) as {
__unmap_source: string | { uri: string; contents: string };
}).__unmap_source,
parsedFrames, parsedFrames,
contextSize contextSize
); );
@ -44,7 +49,7 @@ function getStackFrames(
(functionName == null || (functionName == null ||
functionName.indexOf('__stack_frame_overlay_proxy_console__') === functionName.indexOf('__stack_frame_overlay_proxy_console__') ===
-1) && -1) &&
!toExclude.test(fileName) !toExclude.test(fileName!)
); );
}); });
} }

View File

@ -5,8 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */ function isBultinErrorName(errorName: string | null | undefined) {
function isBultinErrorName(errorName: ?string) {
switch (errorName) { switch (errorName) {
case 'EvalError': case 'EvalError':
case 'InternalError': case 'InternalError':

View File

@ -5,8 +5,10 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */ function isInternalFile(
function isInternalFile(sourceFileName: ?string, fileName: ?string) { sourceFileName: string | null | undefined,
fileName: string | null | undefined
) {
return ( return (
sourceFileName == null || sourceFileName == null ||
sourceFileName === '' || sourceFileName === '' ||

View File

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */
import StackFrame from './stack-frame'; import StackFrame from './stack-frame';
import { getSourceMap } from './getSourceMap'; import { getSourceMap } from './getSourceMap';
import { getLinesAround } from './getLinesAround'; import { getLinesAround } from './getLinesAround';
@ -18,7 +17,7 @@ import { settle } from 'settle-promise';
*/ */
async function map( async function map(
frames: StackFrame[], frames: StackFrame[],
contextLines: number = 3 contextLines = 3
): Promise<StackFrame[]> { ): Promise<StackFrame[]> {
const cache: any = {}; const cache: any = {};
const files: string[] = []; const files: string[] = [];
@ -41,7 +40,7 @@ async function map(
); );
return frames.map((frame) => { return frames.map((frame) => {
const { functionName, fileName, lineNumber, columnNumber } = frame; const { functionName, fileName, lineNumber, columnNumber } = frame;
let { map, fileSource } = cache[fileName] || {}; const { map, fileSource } = cache[fileName!] || {};
if (map == null || lineNumber == null) { if (map == null || lineNumber == null) {
return frame; return frame;
} }

View File

@ -1,11 +1,10 @@
// @flow
import Anser from 'anser'; import Anser from 'anser';
export type ErrorLocation = {| export interface ErrorLocation {
fileName: string, fileName: string;
lineNumber: number, lineNumber: number;
colNumber?: number, colNumber?: number;
|}; }
const filePathRegex = /^\.(\/[^/\n ]+)+\.[^/\n ]+$/; const filePathRegex = /^\.(\/[^/\n ]+)+\.[^/\n ]+$/;
@ -22,11 +21,11 @@ const lineNumberRegexes = [
// Based on error formatting of webpack // Based on error formatting of webpack
// https://github.com/webpack/webpack/blob/v3.5.5/lib/Stats.js#L183-L217 // https://github.com/webpack/webpack/blob/v3.5.5/lib/Stats.js#L183-L217
function parseCompileError(message: string): ?ErrorLocation { function parseCompileError(message: string): ErrorLocation | null | undefined {
const lines: Array<string> = message.split('\n'); const lines: string[] = message.split('\n');
let fileName: string = ''; let fileName = '';
let lineNumber: number = 0; let lineNumber = 0;
let colNumber: number = 0; let colNumber = 0;
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line: string = Anser.ansiToText(lines[i]).trim(); const line: string = Anser.ansiToText(lines[i]).trim();
@ -34,13 +33,15 @@ function parseCompileError(message: string): ?ErrorLocation {
continue; continue;
} }
if (!fileName && line.match(filePathRegex)) { if (!fileName && filePathRegex.exec(line)) {
fileName = line; fileName = line;
} }
let k = 0; let k = 0;
while (k < lineNumberRegexes.length) { while (k < lineNumberRegexes.length) {
const match: ?Array<string> = line.match(lineNumberRegexes[k]); const match: string[] | null | undefined = lineNumberRegexes[k].exec(
line
);
if (match) { if (match) {
lineNumber = parseInt(match[1], 10); lineNumber = parseInt(match[1], 10);
// colNumber starts with 0 and hence add 1 // colNumber starts with 0 and hence add 1

View File

@ -5,14 +5,13 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */
import StackFrame from './stack-frame'; import StackFrame from './stack-frame';
const regexExtractLocation = /\(?(.+?)(?::(\d+))?(?::(\d+))?\)?$/; const regexExtractLocation = /\(?(.+?)(?::(\d+))?(?::(\d+))?\)?$/;
function extractLocation(token: string): [string, number, number] { function extractLocation(token: string): [string, number, number] {
return regexExtractLocation return regexExtractLocation
.exec(token) .exec(token)!
.slice(1) .slice(1)
.map((v) => { .map((v) => {
const p = Number(v); const p = Number(v);
@ -20,7 +19,7 @@ function extractLocation(token: string): [string, number, number] {
return p; return p;
} }
return v; return v;
}); }) as [string, number, number];
} }
const regexValidFrame_Chrome = /^\s*(at|in)\s.+(:\d+)/; const regexValidFrame_Chrome = /^\s*(at|in)\s.+(:\d+)/;
@ -46,7 +45,7 @@ function parseStack(stack: string[]): StackFrame[] {
const last = data.pop(); const last = data.pop();
return new StackFrame( return new StackFrame(
data.join('@') || (isEval ? 'eval' : null), data.join('@') || (isEval ? 'eval' : null),
...extractLocation(last) ...extractLocation(last!)
); );
} else { } else {
// Strip eval, we don't care about it // Strip eval, we don't care about it
@ -58,7 +57,10 @@ function parseStack(stack: string[]): StackFrame[] {
} }
const data = e.trim().split(/\s+/g).slice(1); const data = e.trim().split(/\s+/g).slice(1);
const last = data.pop(); const last = data.pop();
return new StackFrame(data.join(' ') || null, ...extractLocation(last)); return new StackFrame(
data.join(' ') || null,
...extractLocation(last!)
);
} }
}); });
return frames; return frames;

View File

@ -9,6 +9,7 @@ if (typeof Promise === 'undefined') {
// Rejection tracking prevents a common issue where React gets into an // Rejection tracking prevents a common issue where React gets into an
// inconsistent state due to an error, but it gets swallowed by a Promise, // inconsistent state due to an error, but it gets swallowed by a Promise,
// and the user has no idea what causes React's erratic future behavior. // and the user has no idea what causes React's erratic future behavior.
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('promise/lib/rejection-tracking').enable(); require('promise/lib/rejection-tracking').enable();
window.Promise = require('promise/lib/es6-extensions.js'); window.Promise = require('promise/lib/es6-extensions.js');
} }

View File

@ -5,8 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */
/** A container holding a script line. */ /** A container holding a script line. */
class ScriptLine { class ScriptLine {
/** The line number of this line of source. */ /** The line number of this line of source. */
@ -16,7 +14,7 @@ class ScriptLine {
/** Whether or not this line should be highlighted. Particularly useful for error reporting with context. */ /** Whether or not this line should be highlighted. Particularly useful for error reporting with context. */
highlight: boolean; highlight: boolean;
constructor(lineNumber: number, content: string, highlight: boolean = false) { constructor(lineNumber: number, content: string, highlight = false) {
this.lineNumber = lineNumber; this.lineNumber = lineNumber;
this.content = content; this.content = content;
this.highlight = highlight; this.highlight = highlight;
@ -98,10 +96,10 @@ class StackFrame {
str += this.fileName + ':'; str += this.fileName + ':';
} }
if (this.lineNumber != null) { if (this.lineNumber != null) {
str += this.lineNumber + ':'; str += `${this.lineNumber}:`;
} }
if (this.columnNumber != null) { if (this.columnNumber != null) {
str += this.columnNumber + ':'; str += `${this.columnNumber}:`;
} }
return str.slice(0, -1); return str.slice(0, -1);
} }

View File

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* @flow */
import StackFrame from './stack-frame'; import StackFrame from './stack-frame';
import { getSourceMap } from './getSourceMap'; import { getSourceMap } from './getSourceMap';
import { getLinesAround } from './getLinesAround'; import { getLinesAround } from './getLinesAround';
@ -32,16 +31,16 @@ function count(search: string, string: string): number {
* @param {number} [fileContents=3] The number of lines to provide before and after the line specified in the <code>StackFrame</code>. * @param {number} [fileContents=3] The number of lines to provide before and after the line specified in the <code>StackFrame</code>.
*/ */
async function unmap( async function unmap(
_fileUri: string | { uri: string, contents: string }, _fileUri: string | { uri: string; contents: string },
frames: StackFrame[], frames: StackFrame[],
contextLines: number = 3 contextLines = 3
): Promise<StackFrame[]> { ): Promise<StackFrame[]> {
let fileContents = typeof _fileUri === 'object' ? _fileUri.contents : null; let fileContents = typeof _fileUri === 'object' ? _fileUri.contents : null;
let fileUri = typeof _fileUri === 'object' ? _fileUri.uri : _fileUri; const fileUri = typeof _fileUri === 'object' ? _fileUri.uri : _fileUri;
if (fileContents == null) { if (fileContents == null) {
fileContents = await fetch(fileUri).then((res) => res.text()); fileContents = await fetch(fileUri).then((res) => res.text());
} }
const map = await getSourceMap(fileUri, fileContents); const map = await getSourceMap(fileUri, fileContents!);
return frames.map((frame) => { return frames.map((frame) => {
const { const {
functionName, functionName,
@ -104,7 +103,7 @@ async function unmap(
sourceT, sourceT,
lineNumber, lineNumber,
// $FlowFixMe // $FlowFixMe
columnNumber columnNumber!
); );
const originalSource = map.getSource(sourceT); const originalSource = map.getSource(sourceT);
return new StackFrame( return new StackFrame(

View File

@ -0,0 +1,3 @@
declare module 'settle-promise' {
export function settle(promises: Promise<void>[]): Promise<void>;
}

View File

@ -1,55 +0,0 @@
import React from 'react';
import { configure, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import StackTraceTab from '../src/StackTraceTab';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
function genAsyncSnapshot(component, done) {
setTimeout(() => {
component.update();
expect(toJson(component)).toMatchSnapshot();
done();
});
}
const actions = {
0: { type: 'PERFORM_ACTION', action: { type: '@@INIT' } },
1: { type: 'PERFORM_ACTION', action: { type: 'INCREMENT_COUNTER' } },
2: {
type: 'PERFORM_ACTION',
action: { type: 'INCREMENT_COUNTER' },
stack:
'Error\n at fn1 (app.js:72:24)\n at fn2 (app.js:84:31)\n ' +
'at fn3 (chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/page.bundle.js:1269:80)',
},
};
describe('StackTraceTab component', () => {
it('should render with no props', (done) => {
const component = mount(<StackTraceTab />);
genAsyncSnapshot(component, done);
});
it('should render with props, but without stack', (done) => {
const component = mount(
<StackTraceTab actions={actions} action={actions[0].action} />
);
genAsyncSnapshot(component, done);
});
it('should render the link to docs', (done) => {
const component = mount(
<StackTraceTab actions={actions} action={actions[1].action} />
);
genAsyncSnapshot(component, done);
});
it('should render with trace stack', (done) => {
const component = mount(
<StackTraceTab actions={actions} action={actions[2].action} />
);
genAsyncSnapshot(component, done);
});
});

View File

@ -0,0 +1,68 @@
import React, { ReactComponentElement } from 'react';
import { configure, mount, ReactWrapper } from 'enzyme';
import toJson from 'enzyme-to-json';
import StackTraceTab from '../src/StackTraceTab';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
function genAsyncSnapshot(
component: ReactWrapper<any, any, any>,
done: () => void
) {
setTimeout(() => {
component.update();
expect(toJson(component)).toMatchSnapshot();
done();
});
}
const actions = {
0: { type: 'PERFORM_ACTION', action: { type: '@@INIT' } },
1: { type: 'PERFORM_ACTION', action: { type: 'INCREMENT_COUNTER' } },
2: {
type: 'PERFORM_ACTION',
action: { type: 'INCREMENT_COUNTER' },
stack:
'Error\n at fn1 (app.js:72:24)\n at fn2 (app.js:84:31)\n ' +
'at fn3 (chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/page.bundle.js:1269:80)',
},
};
const StackTraceTabAsAny = StackTraceTab as any;
describe('StackTraceTab component', () => {
it('should render with no props', () => {
return new Promise((done) => {
const component = mount(<StackTraceTabAsAny />);
genAsyncSnapshot(component, done);
});
});
it('should render with props, but without stack', () => {
return new Promise((done) => {
const component = mount(
<StackTraceTabAsAny actions={actions} action={actions[0].action} />
);
genAsyncSnapshot(component, done);
});
});
it('should render the link to docs', () => {
return new Promise((done) => {
const component = mount(
<StackTraceTabAsAny actions={actions} action={actions[1].action} />
);
genAsyncSnapshot(component, done);
});
});
it('should render with trace stack', () => {
return new Promise((done) => {
const component = mount(
<StackTraceTabAsAny actions={actions} action={actions[2].action} />
);
genAsyncSnapshot(component, done);
});
});
});

View File

@ -301,12 +301,7 @@ exports[`StackTraceTab component should render with trace stack 1`] = `
} }
} }
> >
<span <span>
onClick={null}
onKeyDown={null}
style={null}
tabIndex={null}
>
app.js:72:24 app.js:72:24
</span> </span>
</div> </div>
@ -345,12 +340,7 @@ exports[`StackTraceTab component should render with trace stack 1`] = `
} }
} }
> >
<span <span>
onClick={null}
onKeyDown={null}
style={null}
tabIndex={null}
>
app.js:84:31 app.js:84:31
</span> </span>
</div> </div>

View File

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.react.base.json",
"include": ["../src", "."]
}

View File

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.react.base.json",
"compilerOptions": {
"outDir": "lib"
},
"include": ["src"]
}

View File

@ -8,6 +8,6 @@
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/33311 // See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/33311
"types": ["node", "jest", "webpack-env"] "types": ["node", "jest", "webpack-env", "chrome"]
} }
} }

View File

@ -378,7 +378,7 @@
"@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-create-class-features-plugin" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-proposal-decorators@^7.10.5", "@babel/plugin-proposal-decorators@^7.8.3": "@babel/plugin-proposal-decorators@^7.8.3":
version "7.10.5" version "7.10.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz#42898bba478bc4b1ae242a703a953a7ad350ffb4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz#42898bba478bc4b1ae242a703a953a7ad350ffb4"
integrity sha512-Sc5TAQSZuLzgY0664mMDn24Vw2P8g/VhyLyGPaWiHahhgLqeZvcGeyBZOrJW0oSKIK2mvQ22a1ENXBIQLhrEiQ== integrity sha512-Sc5TAQSZuLzgY0664mMDn24Vw2P8g/VhyLyGPaWiHahhgLqeZvcGeyBZOrJW0oSKIK2mvQ22a1ENXBIQLhrEiQ==
@ -1110,7 +1110,7 @@
levenary "^1.1.1" levenary "^1.1.1"
semver "^5.5.0" semver "^5.5.0"
"@babel/preset-flow@^7.0.0", "@babel/preset-flow@^7.10.4": "@babel/preset-flow@^7.0.0":
version "7.10.4" version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.10.4.tgz#e0d9c72f8cb02d1633f6a5b7b16763aa2edf659f" resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.10.4.tgz#e0d9c72f8cb02d1633f6a5b7b16763aa2edf659f"
integrity sha512-XI6l1CptQCOBv+ZKYwynyswhtOKwpZZp5n0LG1QKCo8erRhqjoQV6nvx61Eg30JHpysWQSBwA2AWRU3pBbSY5g== integrity sha512-XI6l1CptQCOBv+ZKYwynyswhtOKwpZZp5n0LG1QKCo8erRhqjoQV6nvx61Eg30JHpysWQSBwA2AWRU3pBbSY5g==
@ -3251,6 +3251,11 @@
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
"@types/babel__code-frame@^7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.2.tgz#e0c0f1648cbc09a9d4e5b4ed2ae9a6f7c8f5aeb0"
integrity sha512-imO+jT/yjOKOAS5GQZ8SDtwiIloAGGr6OaZDKB0V5JVaSfGZLat5K5/ZRtyKW6R60XHV3RHYPTFfhYb+wDKyKg==
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
version "7.1.9" version "7.1.9"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d"
@ -3309,6 +3314,14 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/chrome@^0.0.124":
version "0.0.124"
resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.124.tgz#1cdb8e1c1ddb04b15844f5a71b9907f73bbb84a2"
integrity sha512-0UmDQ6A9gaahvztKryIonSTyUMEhuhKNyyJAnBB7ZJN/YXP7YRkL4onPFSTxnIbXcMnYsQFo8TxsGP8jY2mdEw==
dependencies:
"@types/filesystem" "*"
"@types/har-format" "*"
"@types/classnames@^2.2.10": "@types/classnames@^2.2.10":
version "2.2.10" version "2.2.10"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999" resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
@ -3419,6 +3432,18 @@
"@types/qs" "*" "@types/qs" "*"
"@types/serve-static" "*" "@types/serve-static" "*"
"@types/filesystem@*":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.29.tgz#ee3748eb5be140dcf980c3bd35f11aec5f7a3748"
integrity sha512-85/1KfRedmfPGsbK8YzeaQUyV1FQAvMPMTuWFQ5EkLd2w7szhNO96bk3Rh/SKmOfd9co2rCLf0Voy4o7ECBOvw==
dependencies:
"@types/filewriter" "*"
"@types/filewriter@*":
version "0.0.28"
resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.28.tgz#c054e8af4d9dd75db4e63abc76f885168714d4b3"
integrity sha1-wFTor02d11205jq8dviFFocU1LM=
"@types/glob-base@^0.3.0": "@types/glob-base@^0.3.0":
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/@types/glob-base/-/glob-base-0.3.0.tgz#a581d688347e10e50dd7c17d6f2880a10354319d" resolved "https://registry.yarnpkg.com/@types/glob-base/-/glob-base-0.3.0.tgz#a581d688347e10e50dd7c17d6f2880a10354319d"
@ -3439,6 +3464,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/har-format@*":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.4.tgz#3275842095abb60d14b47fa798cc9ff708dab6d4"
integrity sha512-iUxzm1meBm3stxUMzRqgOVHjj4Kgpgu5w9fm4X7kPRfSgVRzythsucEN7/jtOo8SQzm+HfcxWWzJS0mJDH/3DQ==
"@types/hast@^2.0.0": "@types/hast@^2.0.0":
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.1.tgz#b16872f2a6144c7025f296fb9636a667ebb79cd9" resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.1.tgz#b16872f2a6144c7025f296fb9636a667ebb79cd9"
@ -3464,6 +3494,11 @@
"@types/react" "*" "@types/react" "*"
hoist-non-react-statics "^3.3.0" hoist-non-react-statics "^3.3.0"
"@types/html-entities@^1.2.16":
version "1.2.16"
resolved "https://registry.yarnpkg.com/@types/html-entities/-/html-entities-1.2.16.tgz#4d1fe208c4c33727ac4657e6f5d92bfe52427023"
integrity sha512-CI6fHfFvkTtX2Nlr4JBA6yIFTfA4p9E6w9ky64X6PrfXiTALhUh/SOa+Sxvv2p87m+y5AH71lAUrx0lSYx4hKQ==
"@types/html-minifier-terser@^5.0.0": "@types/html-minifier-terser@^5.0.0":
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880"