Skip to content

Commit

Permalink
feat: add in debug method
Browse files Browse the repository at this point in the history
Closes #5
  • Loading branch information
crutchcorn committed Dec 13, 2021
1 parent b34b0b0 commit e745024
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 4 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,22 @@
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"chalk": "^4.1.0",
"jest-matcher-utils": "^27.4.2",
"lz-string": "^1.4.4",
"pretty-format": "^27.0.2",
"redent": "^3.0.0",
"slice-ansi": "^4.0.0",
"strip-ansi": "^6.0.1",
"strip-final-newline": "^2.0.0",
"tree-kill": "^1.2.2"
},
"devDependencies": {
"@types/lz-string": "^1.3.34",
"@types/strip-final-newline": "^3.0.0",
"chalk": "^4.1.2",
"has-ansi": "^3.0.0",
"inquirer": "^8.2.0",
"jest-in-case": "^1.0.2",
"jest-snapshot-serializer-ansi": "^1.0.0",
"jest-watch-select-projects": "^2.0.0",
"kcd-scripts": "^11.2.2",
"typescript": "^4.1.2"
Expand Down
7 changes: 7 additions & 0 deletions src/__tests__/execute-scripts/log-output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const chalk = require('chalk');
const customChalk = new chalk.Instance({level: 1});

console.log("__disable_ansi_serialization");

// eslint-disable-next-line prefer-template
console.log(customChalk.blue('Hello') + ' World' + customChalk.red('!'));
82 changes: 82 additions & 0 deletions src/__tests__/get-user-code-frame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import fs from 'fs'
import {getUserCodeFrame} from '../get-user-code-frame'

jest.mock('fs', () => ({
// We setup the contents of a sample file
readFileSync: jest.fn(
() => `
import {screen} from '@testing-library/dom'
it('renders', () => {
document.body.appendChild(
document.createTextNode('Hello world')
)
screen.debug()
expect(screen.getByText('Hello world')).toBeInTheDocument()
})
`,
),
}))

const userStackFrame = 'at somethingWrong (/sample-error/error-example.js:7:14)'

let globalErrorMock

beforeEach(() => {
// Mock global.Error so we can setup our own stack messages
globalErrorMock = jest.spyOn(global, 'Error')
})

afterEach(() => {
global.Error.mockRestore()
})

test('it returns only user code frame when code frames from node_modules are first', () => {
const stack = `Error: Kaboom
at Object.<anonymous> (/sample-error/node_modules/@es2050/console/build/index.js:4:10)
${userStackFrame}
`
globalErrorMock.mockImplementationOnce(() => ({stack}))
const userTrace = getUserCodeFrame(stack)

expect(userTrace).toMatchInlineSnapshot(`
/sample-error/error-example.js:7:14
5 | document.createTextNode('Hello world')
6 | )
> 7 | screen.debug()
| ^
`)
})

test('it returns only user code frame when node code frames are present afterwards', () => {
const stack = `Error: Kaboom
at Object.<anonymous> (/sample-error/node_modules/@es2050/console/build/index.js:4:10)
${userStackFrame}
at Object.<anonymous> (/sample-error/error-example.js:14:1)
at internal/main/run_main_module.js:17:47
`
globalErrorMock.mockImplementationOnce(() => ({stack}))
const userTrace = getUserCodeFrame()

expect(userTrace).toMatchInlineSnapshot(`
/sample-error/error-example.js:7:14
5 | document.createTextNode('Hello world')
6 | )
> 7 | screen.debug()
| ^
`)
})

test("it returns empty string if file from code frame can't be read", () => {
// Make fire read purposely fail
fs.readFileSync.mockImplementationOnce(() => {
throw Error()
})
const stack = `Error: Kaboom
${userStackFrame}
`
globalErrorMock.mockImplementationOnce(() => ({stack}))

expect(getUserCodeFrame(stack)).toEqual('')
})
30 changes: 30 additions & 0 deletions src/__tests__/pretty-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const {resolve} = require('path')
const {render} = require('../pure')
const {prettyCLI} = require('../pretty-cli')


test('Should pretty print with ANSI codes properly', async () => {
const instance = await render('node', [
resolve(__dirname, './execute-scripts/log-output.js'),
])

await instance.findByText('Hello')

expect(prettyCLI(instance, 9000)).toMatchInlineSnapshot(`
__disable_ansi_serialization
Hello World!
`)
})

test('Should escape ANSI codes properly when sliced too thin', async () => {
const instance = await render('node', [
resolve(__dirname, './execute-scripts/log-output.js'),
])

await instance.findByText('Hello')

expect(prettyCLI(instance, 30)).toMatchInlineSnapshot(`
__disable_ansi_serialization
H
`)
})
67 changes: 67 additions & 0 deletions src/get-user-code-frame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// We try to load node dependencies
let chalk = null
let readFileSync = null
let codeFrameColumns = null

try {
const nodeRequire = module && module.require

readFileSync = nodeRequire.call(module, 'fs').readFileSync
codeFrameColumns = nodeRequire.call(
module,
'@babel/code-frame',
).codeFrameColumns
chalk = nodeRequire.call(module, 'chalk')
} catch {
// We're in a browser environment
}

// frame has the form "at myMethod (location/to/my/file.js:10:2)"
function getCodeFrame(frame) {
const locationStart = frame.indexOf('(') + 1
const locationEnd = frame.indexOf(')')
const frameLocation = frame.slice(locationStart, locationEnd)

const frameLocationElements = frameLocation.split(':')
const [filename, line, column] = [
frameLocationElements[0],
parseInt(frameLocationElements[1], 10),
parseInt(frameLocationElements[2], 10),
]

let rawFileContents = ''
try {
rawFileContents = readFileSync(filename, 'utf-8')
} catch {
return ''
}

const codeFrame = codeFrameColumns(
rawFileContents,
{
start: {line, column},
},
{
highlightCode: true,
linesBelow: 0,
},
)
return `${chalk.dim(frameLocation)}\n${codeFrame}\n`
}

function getUserCodeFrame() {
// If we couldn't load dependencies, we can't generate the user trace
/* istanbul ignore next */
if (!readFileSync || !codeFrameColumns) {
return ''
}
const err = new Error()
const firstClientCodeFrame = err.stack
.split('\n')
.slice(1) // Remove first line which has the form "Error: TypeError"
.find(frame => !frame.includes('node_modules/')) // Ignore frames from 3rd party libraries

return getCodeFrame(firstClientCodeFrame)
}

export {getUserCodeFrame}
37 changes: 37 additions & 0 deletions src/pretty-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import sliceAnsi from 'slice-ansi';
import {getUserCodeFrame} from './get-user-code-frame'

function prettyCLI(testInstance, maxLength) {
if (typeof maxLength !== 'number') {
maxLength =
(typeof process !== 'undefined' && process.env.DEBUG_PRINT_LIMIT) || 7000
}

if (maxLength === 0) {
return ''
}

if (!('stdoutArr' in testInstance)) {
throw new TypeError(
`Expected an instance but got ${testInstance}`,
)
}

const outStr = testInstance.stdoutArr.join('\n');

// eslint-disable-next-line no-negated-condition
return maxLength !== undefined && outStr.length > maxLength
? sliceAnsi(outStr, 0, maxLength)
: outStr
}

const logCLI = (...args) => {
const userCodeFrame = getUserCodeFrame()
if (userCodeFrame) {
process.stdout.write(`${prettyCLI(...args)}\n\n${userCodeFrame}`)
} else {
process.stdout.write(prettyCLI(...args))
}
}

export {prettyCLI, logCLI}
4 changes: 4 additions & 0 deletions src/pure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import userEvent from './user-event'
import {bindObjectFnsToInstance, setCurrentInstance} from './helpers'
import {fireEvent} from './events'
import {getConfig} from './config'
import {logCLI} from "./pretty-cli";

const mountedInstances = new Set<TestInstance>()

Expand Down Expand Up @@ -40,6 +41,9 @@ async function render(
clear() {
execOutputAPI.stdoutArr = []
},
debug(maxLength?: number) {
logCLI(execOutputAPI, maxLength)
},
// An array of strings gathered from stdout when unable to do
// `await stdout` because of inquirer interactive prompts
stdoutArr: [] as Array<string | Buffer>,
Expand Down
13 changes: 11 additions & 2 deletions tests/setup-env.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import jestSnapshotSerializerAnsi from 'jest-snapshot-serializer-ansi'
import '../src/extend-expect';
import {configure, getConfig} from "../src/config";
import hasAnsi from 'has-ansi';
import stripAnsi from 'strip-ansi';

/**
* We have instances where we need to disable this serializer to test for ANSI codes
* @see jest-snapshot-serializer-ansi
*/
expect.addSnapshotSerializer({
test: value => typeof value === 'string' && !value.includes("__disable_ansi_serialization") && hasAnsi(value),
print: (value, serialize) => serialize(stripAnsi(value)),
});

expect.addSnapshotSerializer(jestSnapshotSerializerAnsi)
// add serializer for MutationRecord
expect.addSnapshotSerializer({
print: (record, serialize) => {
Expand Down
1 change: 1 addition & 0 deletions types/pure.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface TestInstance {
stdoutArr: Array<string, Buffer>
stderrArr: Array<string, Buffer>
hasExit(): null | {exitCode: number}
debug(maxLength?: number): void
}

export interface RenderOptions {
Expand Down

0 comments on commit e745024

Please sign in to comment.