Skip to content

Commit dfb4aa1

Browse files
feat(debug): show line and codeframe when using debug (#733)
Closes #440
1 parent 19901e1 commit dfb4aa1

File tree

6 files changed

+198
-3
lines changed

6 files changed

+198
-3
lines changed

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@
3838
"types"
3939
],
4040
"dependencies": {
41+
"@babel/code-frame": "^7.10.4",
4142
"@babel/runtime": "^7.10.3",
4243
"@types/aria-query": "^4.2.0",
4344
"aria-query": "^4.2.2",
4445
"dom-accessibility-api": "^0.5.1",
45-
"pretty-format": "^26.4.2"
46+
"pretty-format": "^26.4.2",
47+
"chalk": "^4.1.0"
4648
},
4749
"devDependencies": {
4850
"@testing-library/jest-dom": "^5.10.1",
@@ -76,4 +78,4 @@
7678
"url": "https://github.com/testing-library/dom-testing-library/issues"
7779
},
7880
"homepage": "https://github.com/testing-library/dom-testing-library#readme"
79-
}
81+
}

src/__tests__/get-user-code-frame.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import fs from 'fs'
2+
import {getUserCodeFrame} from '../get-user-code-frame'
3+
4+
jest.mock('fs', () => ({
5+
// We setup the contents of a sample file
6+
readFileSync: jest.fn(
7+
() => `
8+
import {screen} from '@testing-library/dom'
9+
it('renders', () => {
10+
document.body.appendChild(
11+
document.createTextNode('Hello world')
12+
)
13+
screen.debug()
14+
expect(screen.getByText('Hello world')).toBeInTheDocument()
15+
})
16+
`,
17+
),
18+
}))
19+
20+
const userStackFrame = 'at somethingWrong (/sample-error/error-example.js:7:14)'
21+
22+
let globalErrorMock
23+
24+
beforeEach(() => {
25+
// Mock global.Error so we can setup our own stack messages
26+
globalErrorMock = jest.spyOn(global, 'Error')
27+
})
28+
29+
afterEach(() => {
30+
global.Error.mockRestore()
31+
})
32+
33+
test('it returns only user code frame when code frames from node_modules are first', () => {
34+
const stack = `Error: Kaboom
35+
at Object.<anonymous> (/sample-error/node_modules/@es2050/console/build/index.js:4:10)
36+
${userStackFrame}
37+
`
38+
globalErrorMock.mockImplementationOnce(() => ({stack}))
39+
const userTrace = getUserCodeFrame(stack)
40+
41+
expect(userTrace).toMatchInlineSnapshot(`
42+
"/sample-error/error-example.js:7:14
43+
5 | document.createTextNode('Hello world')
44+
6 | )
45+
> 7 | screen.debug()
46+
| ^
47+
"
48+
`)
49+
})
50+
51+
test('it returns only user code frame when node code frames are present afterwards', () => {
52+
const stack = `Error: Kaboom
53+
at Object.<anonymous> (/sample-error/node_modules/@es2050/console/build/index.js:4:10)
54+
${userStackFrame}
55+
at Object.<anonymous> (/sample-error/error-example.js:14:1)
56+
at internal/main/run_main_module.js:17:47
57+
`
58+
globalErrorMock.mockImplementationOnce(() => ({stack}))
59+
const userTrace = getUserCodeFrame()
60+
61+
expect(userTrace).toMatchInlineSnapshot(`
62+
"/sample-error/error-example.js:7:14
63+
5 | document.createTextNode('Hello world')
64+
6 | )
65+
> 7 | screen.debug()
66+
| ^
67+
"
68+
`)
69+
})
70+
71+
test("it returns empty string if file from code frame can't be read", () => {
72+
// Make fire read purposely fail
73+
fs.readFileSync.mockImplementationOnce(() => {
74+
throw Error()
75+
})
76+
const stack = `Error: Kaboom
77+
${userStackFrame}
78+
`
79+
globalErrorMock.mockImplementationOnce(() => ({stack}))
80+
81+
expect(getUserCodeFrame(stack)).toEqual('')
82+
})

src/__tests__/pretty-dom.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {prettyDOM, logDOM} from '../pretty-dom'
2+
import {getUserCodeFrame} from '../get-user-code-frame'
23
import {render, renderIntoDocument} from './helpers/test-utils'
34

5+
jest.mock('../get-user-code-frame')
6+
47
beforeEach(() => {
58
jest.spyOn(console, 'log').mockImplementation(() => {})
69
})
@@ -60,6 +63,36 @@ test('logDOM logs prettyDOM to the console', () => {
6063
`)
6164
})
6265

66+
test('logDOM logs prettyDOM with code frame to the console', () => {
67+
getUserCodeFrame.mockImplementationOnce(
68+
() => `"/home/john/projects/sample-error/error-example.js:7:14
69+
5 | document.createTextNode('Hello World!')
70+
6 | )
71+
> 7 | screen.debug()
72+
| ^
73+
"
74+
`,
75+
)
76+
const {container} = render('<div>Hello World!</div>')
77+
logDOM(container)
78+
expect(console.log).toHaveBeenCalledTimes(1)
79+
expect(console.log.mock.calls[0][0]).toMatchInlineSnapshot(`
80+
"<div>
81+
<div>
82+
Hello World!
83+
</div>
84+
</div>
85+
86+
"/home/john/projects/sample-error/error-example.js:7:14
87+
5 | document.createTextNode('Hello World!')
88+
6 | )
89+
> 7 | screen.debug()
90+
| ^
91+
"
92+
"
93+
`)
94+
})
95+
6396
describe('prettyDOM fails with first parameter without outerHTML field', () => {
6497
test('with array', () => {
6598
expect(() => prettyDOM(['outerHTML'])).toThrowErrorMatchingInlineSnapshot(

src/__tests__/screen.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import {screen} from '..'
22
import {renderIntoDocument} from './helpers/test-utils'
33

4+
// Since screen.debug internally calls getUserCodeFrame, we mock it so it doesn't affect these tests
5+
jest.mock('../get-user-code-frame', () => ({
6+
getUserCodeFrame: () => '',
7+
}))
8+
49
beforeEach(() => {
510
jest.spyOn(console, 'log').mockImplementation(() => {})
611
})

src/get-user-code-frame.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// We try to load node dependencies
2+
let chalk = null
3+
let readFileSync = null
4+
let codeFrameColumns = null
5+
6+
try {
7+
const nodeRequire = module && module.require
8+
9+
readFileSync = nodeRequire.call(module, 'fs').readFileSync
10+
codeFrameColumns = nodeRequire.call(module, '@babel/code-frame')
11+
.codeFrameColumns
12+
chalk = nodeRequire.call(module, 'chalk')
13+
} catch {
14+
// We're in a browser environment
15+
}
16+
17+
// frame has the form "at myMethod (location/to/my/file.js:10:2)"
18+
function getCodeFrame(frame) {
19+
const locationStart = frame.indexOf('(') + 1
20+
const locationEnd = frame.indexOf(')')
21+
const frameLocation = frame.slice(locationStart, locationEnd)
22+
23+
const frameLocationElements = frameLocation.split(':')
24+
const [filename, line, column] = [
25+
frameLocationElements[0],
26+
parseInt(frameLocationElements[1], 10),
27+
parseInt(frameLocationElements[2], 10),
28+
]
29+
30+
let rawFileContents = ''
31+
try {
32+
rawFileContents = readFileSync(filename, 'utf-8')
33+
} catch {
34+
return ''
35+
}
36+
37+
const codeFrame = codeFrameColumns(
38+
rawFileContents,
39+
{
40+
start: {line, column},
41+
},
42+
{
43+
highlightCode: true,
44+
linesBelow: 0,
45+
},
46+
)
47+
return `${chalk.dim(frameLocation)}\n${codeFrame}\n`
48+
}
49+
50+
function getUserCodeFrame() {
51+
// If we couldn't load dependencies, we can't generate the user trace
52+
/* istanbul ignore next */
53+
if (!readFileSync || !codeFrameColumns) {
54+
return ''
55+
}
56+
const err = new Error()
57+
const firstClientCodeFrame = err.stack
58+
.split('\n')
59+
.slice(1) // Remove first line which has the form "Error: TypeError"
60+
.find(frame => !frame.includes('node_modules/')) // Ignore frames from 3rd party libraries
61+
62+
return getCodeFrame(firstClientCodeFrame)
63+
}
64+
65+
export {getUserCodeFrame}

src/pretty-dom.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import prettyFormat from 'pretty-format'
2+
import {getUserCodeFrame} from './get-user-code-frame'
23
import {getDocument} from './helpers'
34

45
function inCypress(dom) {
@@ -61,6 +62,13 @@ function prettyDOM(dom, maxLength, options) {
6162
: debugContent
6263
}
6364

64-
const logDOM = (...args) => console.log(prettyDOM(...args))
65+
const logDOM = (...args) => {
66+
const userCodeFrame = getUserCodeFrame()
67+
if (userCodeFrame) {
68+
console.log(`${prettyDOM(...args)}\n\n${userCodeFrame}`)
69+
} else {
70+
console.log(prettyDOM(...args))
71+
}
72+
}
6573

6674
export {prettyDOM, logDOM}

0 commit comments

Comments
 (0)