From e74502494a8575f4e9f23c007cc42a3321f9dd37 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 13 Dec 2021 08:59:33 -0800 Subject: [PATCH] feat: add in debug method Closes #5 --- package.json | 5 +- src/__tests__/execute-scripts/log-output.js | 7 ++ src/__tests__/get-user-code-frame.js | 82 +++++++++++++++++++++ src/__tests__/pretty-cli.js | 30 ++++++++ src/get-user-code-frame.js | 67 +++++++++++++++++ src/pretty-cli.js | 37 ++++++++++ src/pure.ts | 4 + tests/setup-env.js | 13 +++- types/pure.d.ts | 1 + 9 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/execute-scripts/log-output.js create mode 100644 src/__tests__/get-user-code-frame.js create mode 100644 src/__tests__/pretty-cli.js create mode 100644 src/get-user-code-frame.js create mode 100644 src/pretty-cli.js diff --git a/package.json b/package.json index 64c5197..010b53e 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,11 @@ "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" @@ -54,9 +54,10 @@ "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" diff --git a/src/__tests__/execute-scripts/log-output.js b/src/__tests__/execute-scripts/log-output.js new file mode 100644 index 0000000..6cf09c7 --- /dev/null +++ b/src/__tests__/execute-scripts/log-output.js @@ -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('!')); diff --git a/src/__tests__/get-user-code-frame.js b/src/__tests__/get-user-code-frame.js new file mode 100644 index 0000000..8d2bd05 --- /dev/null +++ b/src/__tests__/get-user-code-frame.js @@ -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. (/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. (/sample-error/node_modules/@es2050/console/build/index.js:4:10) + ${userStackFrame} + at Object. (/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('') +}) diff --git a/src/__tests__/pretty-cli.js b/src/__tests__/pretty-cli.js new file mode 100644 index 0000000..45262c9 --- /dev/null +++ b/src/__tests__/pretty-cli.js @@ -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 + `) +}) diff --git a/src/get-user-code-frame.js b/src/get-user-code-frame.js new file mode 100644 index 0000000..32de6a8 --- /dev/null +++ b/src/get-user-code-frame.js @@ -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} diff --git a/src/pretty-cli.js b/src/pretty-cli.js new file mode 100644 index 0000000..3abd787 --- /dev/null +++ b/src/pretty-cli.js @@ -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} diff --git a/src/pure.ts b/src/pure.ts index 5862eee..884899e 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -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() @@ -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, diff --git a/tests/setup-env.js b/tests/setup-env.js index 05a7687..77d9c2b 100644 --- a/tests/setup-env.js +++ b/tests/setup-env.js @@ -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) => { diff --git a/types/pure.d.ts b/types/pure.d.ts index 35aeef6..97b4c2f 100644 --- a/types/pure.d.ts +++ b/types/pure.d.ts @@ -9,6 +9,7 @@ export interface TestInstance { stdoutArr: Array stderrArr: Array hasExit(): null | {exitCode: number} + debug(maxLength?: number): void } export interface RenderOptions {