Skip to content

Commit e745024

Browse files
committed
feat: add in debug method
Closes #5
1 parent b34b0b0 commit e745024

File tree

9 files changed

+242
-4
lines changed

9 files changed

+242
-4
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,22 @@
4242
"dependencies": {
4343
"@babel/code-frame": "^7.10.4",
4444
"@babel/runtime": "^7.12.5",
45-
"chalk": "^4.1.0",
4645
"jest-matcher-utils": "^27.4.2",
4746
"lz-string": "^1.4.4",
4847
"pretty-format": "^27.0.2",
4948
"redent": "^3.0.0",
49+
"slice-ansi": "^4.0.0",
5050
"strip-ansi": "^6.0.1",
5151
"strip-final-newline": "^2.0.0",
5252
"tree-kill": "^1.2.2"
5353
},
5454
"devDependencies": {
5555
"@types/lz-string": "^1.3.34",
5656
"@types/strip-final-newline": "^3.0.0",
57+
"chalk": "^4.1.2",
58+
"has-ansi": "^3.0.0",
5759
"inquirer": "^8.2.0",
5860
"jest-in-case": "^1.0.2",
59-
"jest-snapshot-serializer-ansi": "^1.0.0",
6061
"jest-watch-select-projects": "^2.0.0",
6162
"kcd-scripts": "^11.2.2",
6263
"typescript": "^4.1.2"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const chalk = require('chalk');
2+
const customChalk = new chalk.Instance({level: 1});
3+
4+
console.log("__disable_ansi_serialization");
5+
6+
// eslint-disable-next-line prefer-template
7+
console.log(customChalk.blue('Hello') + ' World' + customChalk.red('!'));
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-cli.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const {resolve} = require('path')
2+
const {render} = require('../pure')
3+
const {prettyCLI} = require('../pretty-cli')
4+
5+
6+
test('Should pretty print with ANSI codes properly', async () => {
7+
const instance = await render('node', [
8+
resolve(__dirname, './execute-scripts/log-output.js'),
9+
])
10+
11+
await instance.findByText('Hello')
12+
13+
expect(prettyCLI(instance, 9000)).toMatchInlineSnapshot(`
14+
__disable_ansi_serialization
15+
Hello World!
16+
`)
17+
})
18+
19+
test('Should escape ANSI codes properly when sliced too thin', async () => {
20+
const instance = await render('node', [
21+
resolve(__dirname, './execute-scripts/log-output.js'),
22+
])
23+
24+
await instance.findByText('Hello')
25+
26+
expect(prettyCLI(instance, 30)).toMatchInlineSnapshot(`
27+
__disable_ansi_serialization
28+
H
29+
`)
30+
})

src/get-user-code-frame.js

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

src/pretty-cli.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import sliceAnsi from 'slice-ansi';
2+
import {getUserCodeFrame} from './get-user-code-frame'
3+
4+
function prettyCLI(testInstance, maxLength) {
5+
if (typeof maxLength !== 'number') {
6+
maxLength =
7+
(typeof process !== 'undefined' && process.env.DEBUG_PRINT_LIMIT) || 7000
8+
}
9+
10+
if (maxLength === 0) {
11+
return ''
12+
}
13+
14+
if (!('stdoutArr' in testInstance)) {
15+
throw new TypeError(
16+
`Expected an instance but got ${testInstance}`,
17+
)
18+
}
19+
20+
const outStr = testInstance.stdoutArr.join('\n');
21+
22+
// eslint-disable-next-line no-negated-condition
23+
return maxLength !== undefined && outStr.length > maxLength
24+
? sliceAnsi(outStr, 0, maxLength)
25+
: outStr
26+
}
27+
28+
const logCLI = (...args) => {
29+
const userCodeFrame = getUserCodeFrame()
30+
if (userCodeFrame) {
31+
process.stdout.write(`${prettyCLI(...args)}\n\n${userCodeFrame}`)
32+
} else {
33+
process.stdout.write(prettyCLI(...args))
34+
}
35+
}
36+
37+
export {prettyCLI, logCLI}

src/pure.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import userEvent from './user-event'
77
import {bindObjectFnsToInstance, setCurrentInstance} from './helpers'
88
import {fireEvent} from './events'
99
import {getConfig} from './config'
10+
import {logCLI} from "./pretty-cli";
1011

1112
const mountedInstances = new Set<TestInstance>()
1213

@@ -40,6 +41,9 @@ async function render(
4041
clear() {
4142
execOutputAPI.stdoutArr = []
4243
},
44+
debug(maxLength?: number) {
45+
logCLI(execOutputAPI, maxLength)
46+
},
4347
// An array of strings gathered from stdout when unable to do
4448
// `await stdout` because of inquirer interactive prompts
4549
stdoutArr: [] as Array<string | Buffer>,

tests/setup-env.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
import jestSnapshotSerializerAnsi from 'jest-snapshot-serializer-ansi'
21
import '../src/extend-expect';
32
import {configure, getConfig} from "../src/config";
3+
import hasAnsi from 'has-ansi';
4+
import stripAnsi from 'strip-ansi';
5+
6+
/**
7+
* We have instances where we need to disable this serializer to test for ANSI codes
8+
* @see jest-snapshot-serializer-ansi
9+
*/
10+
expect.addSnapshotSerializer({
11+
test: value => typeof value === 'string' && !value.includes("__disable_ansi_serialization") && hasAnsi(value),
12+
print: (value, serialize) => serialize(stripAnsi(value)),
13+
});
414

5-
expect.addSnapshotSerializer(jestSnapshotSerializerAnsi)
615
// add serializer for MutationRecord
716
expect.addSnapshotSerializer({
817
print: (record, serialize) => {

types/pure.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface TestInstance {
99
stdoutArr: Array<string, Buffer>
1010
stderrArr: Array<string, Buffer>
1111
hasExit(): null | {exitCode: number}
12+
debug(maxLength?: number): void
1213
}
1314

1415
export interface RenderOptions {

0 commit comments

Comments
 (0)