diff --git a/docs/contributors/testing-overview.md b/docs/contributors/testing-overview.md index 5c7d2a642b81b..6e72a4325c6ac 100644 --- a/docs/contributors/testing-overview.md +++ b/docs/contributors/testing-overview.md @@ -357,6 +357,11 @@ Sometimes we need to mock refs for some stories which use them. Check the follow - [Using createNodeMock to mock refs](https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-core#using-createnodemock-to-mock-refs) with StoryShots. In that case, you might see test failures and `TypeError` reported by Jest in the lines which try to access a property from `ref.current`. If this happens, search for `initStoryshots` method call, which contains all necessary configurations to adjust. + +### Debugging Jest unit tests + +Running `npm run test-unit:debug` will start the tests in debug mode so a [node inspector client](https://nodejs.org/en/docs/guides/debugging-getting-started/#inspector-clients) can connect to the process and inspect the execution. Instructions for using Google Chrome or Visual Studio Code as an inspector client can be found in the [wp-scripts documentation](/packages/scripts/README.md#debugging-jest-unit-tests). + ## Native mobile testing Part of the unit-tests suite is a set of Jest tests run exercise native-mobile codepaths, developed in React Native. Since those tests run on Node, they can be launched locally on your development machine without the need for specific native Android or iOS dev tools or SDKs. It also means that they can be debugged using typical dev tools. Read on for instructions how to debug. diff --git a/package.json b/package.json index b2a95e5213b51..8680f1359baa2 100644 --- a/package.json +++ b/package.json @@ -225,6 +225,7 @@ "test-performance": "wp-scripts test-e2e --config packages/e2e-tests/jest.performance.config.js", "test-php": "npm run lint-php && npm run test-unit-php", "test-unit": "wp-scripts test-unit-js --config test/unit/jest.config.js", + "test-unit:debug": "wp-scripts --inspect-brk test-unit-js --runInBand --no-cache --verbose --config test/unit/jest.config.js ", "test-unit:update": "npm run test-unit -- --updateSnapshot", "test-unit:watch": "npm run test-unit -- --watch", "test-unit-php": "wp-scripts env test-php", diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 3c49a764787ed..048bcf272d556 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -425,7 +425,8 @@ _Example:_ "scripts": { "test:unit": "wp-scripts test-unit-js", "test:unit:help": "wp-scripts test-unit-js --help", - "test:unit:watch": "wp-scripts test-unit-js --watch" + "test:unit:watch": "wp-scripts test-unit-js --watch", + "test:unit:debug": "wp-scripts --inspect-brk test-unit-js --runInBand --no-cache" } } ``` @@ -435,6 +436,7 @@ This is how you execute those scripts using the presented setup: * `npm run test:unit` - runs all unit tests. * `npm run test:unit:help` - prints all available options to configure unit tests runner. * `npm run test:unit:watch` - runs all unit tests in the watch mode. +* `npm run test:unit:debug` - runs all unit tests with the node debugger enabled. Jest will look for test files with any of the following popular naming conventions: @@ -442,6 +444,34 @@ Jest will look for test files with any of the following popular naming conventio - Files with `.js` (or `.ts`) suffix directly located in `test` folders. - Files with `.test.js` (or `.test.ts`) suffix. +#### Debugging Jest unit tests + +Tests can be debugged by any [inspector client](https://nodejs.org/en/docs/guides/debugging-getting-started/#inspector-clients) that supports the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). + +Follow the instructions for debugging Node.js with your favorite supported browser or IDE. When the instructions say to use `node --inspect script.js` or `node --inspect-brk script.js`, simply use `wp-scripts --inspect test-unit-js` or `wp-scripts --inspect-brk test-unit-js` instead. + +Google Chrome and Visual Studio Code are used as examples below. + +##### Debugging in Google Chrome + +Place `debugger;` statements in any test and run `npm run test:unit:debug`. + +Then open `about:inspect` in Google Chrome and select `inspect` on your process. + +A breakpoint will be set at the first line of the script (this is done to give you time to open the developer tools and to prevent Jest from executing before you have time to do so). Click the resume button in the upper right panel of the dev tools to continue execution. When Jest executes the test that contains the debugger statement, execution will pause and you can examine the current scope and call stack. + +##### Debugging in Visual Studio Code + +Debugging NPM scripts is supported out of the box for Visual Studio Code as of [version 1.23](https://code.visualstudio.com/blogs/2018/07/12/introducing-logpoints-and-auto-attach#_npm-scripts-and-debugging) and can be used to debug Jest unit tests. + +First, set a breakpoint in your tests by clicking on a line in the editor's left margin by the line numbers. + +Then open NPM Scripts in the Explorer or run `Explorer: Focus on NPM Scripts View` in the command palette to see the NPM scripts. To start the tests, click the debug icon next to `test:unit:debug`. + +The tests will start running, and execution will pause on your selected line so you can inspect the current scope and call stack within the editor. + +See [Debugging in Visual Studio Code](https://code.visualstudio.com/Docs/editor/debugging) for more details on using the Visual Studio Code debugger. + #### Advanced information It uses [Jest](https://jestjs.io/) behind the scenes and you are able to use all of its [CLI options](https://jestjs.io/docs/en/cli.html). You can also run `./node_modules/.bin/wp-scripts test:unit --help` or `npm run test:unit:help` (as mentioned above) to view all of the available options. By default, it uses the set of recommended options defined in [@wordpress/jest-preset-default](https://www.npmjs.com/package/@wordpress/jest-preset-default) npm package. You can override them with your own options as described in [Jest documentation](https://jestjs.io/docs/en/configuration). Learn more in the [Advanced Usage](#advanced-usage) section. diff --git a/packages/scripts/bin/wp-scripts.js b/packages/scripts/bin/wp-scripts.js index a74076ead7f85..d99d7c0c9fcae 100755 --- a/packages/scripts/bin/wp-scripts.js +++ b/packages/scripts/bin/wp-scripts.js @@ -3,8 +3,8 @@ /** * Internal dependencies */ -const { getArgsFromCLI, spawnScript } = require( '../utils' ); +const { getNodeArgsFromCLI, spawnScript } = require( '../utils' ); -const [ scriptName, ...nodesArgs ] = getArgsFromCLI(); +const { scriptName, scriptArgs, nodeArgs } = getNodeArgsFromCLI(); -spawnScript( scriptName, nodesArgs ); +spawnScript( scriptName, scriptArgs, nodeArgs ); diff --git a/packages/scripts/utils/cli.js b/packages/scripts/utils/cli.js index c1572ece8a23c..5f889e7543fa9 100644 --- a/packages/scripts/utils/cli.js +++ b/packages/scripts/utils/cli.js @@ -7,7 +7,7 @@ const spawn = require( 'cross-spawn' ); /** * Internal dependencies */ -const { fromScriptsRoot, hasScriptFile } = require( './file' ); +const { fromScriptsRoot, hasScriptFile, getScripts } = require( './file' ); const { exit, getArgsFromCLI } = require( './process' ); const getArgFromCLI = ( arg ) => { @@ -23,6 +23,17 @@ const hasArgInCLI = ( arg ) => getArgFromCLI( arg ) !== undefined; const getFileArgsFromCLI = () => minimist( getArgsFromCLI() )._; +const getNodeArgsFromCLI = () => { + const args = getArgsFromCLI(); + const scripts = getScripts(); + const scriptIndex = args.findIndex( ( arg ) => scripts.includes( arg ) ); + return { + nodeArgs: args.slice( 0, scriptIndex ), + scriptName: args[ scriptIndex ], + scriptArgs: args.slice( scriptIndex + 1 ), + }; +}; + const hasFileArgInCLI = () => getFileArgsFromCLI().length > 0; const handleSignal = ( signal ) => { @@ -44,7 +55,7 @@ const handleSignal = ( signal ) => { exit( 1 ); }; -const spawnScript = ( scriptName, args = [] ) => { +const spawnScript = ( scriptName, args = [], nodeArgs = [] ) => { if ( ! scriptName ) { // eslint-disable-next-line no-console console.log( 'Script name is missing.' ); @@ -64,7 +75,7 @@ const spawnScript = ( scriptName, args = [] ) => { const { signal, status } = spawn.sync( 'node', - [ fromScriptsRoot( scriptName ), ...args ], + [ ...nodeArgs, fromScriptsRoot( scriptName ), ...args ], { stdio: 'inherit', } @@ -81,6 +92,7 @@ module.exports = { getArgFromCLI, getArgsFromCLI, getFileArgsFromCLI, + getNodeArgsFromCLI, hasArgInCLI, hasFileArgInCLI, spawnScript, diff --git a/packages/scripts/utils/file.js b/packages/scripts/utils/file.js index 24dd8ad505e4b..6e5718210e66b 100644 --- a/packages/scripts/utils/file.js +++ b/packages/scripts/utils/file.js @@ -1,7 +1,7 @@ /** * External dependencies */ -const { existsSync } = require( 'fs' ); +const { existsSync, readdirSync } = require( 'fs' ); const path = require( 'path' ); /** @@ -24,10 +24,16 @@ const fromScriptsRoot = ( scriptName ) => const hasScriptFile = ( scriptName ) => existsSync( fromScriptsRoot( scriptName ) ); +const getScripts = () => + readdirSync( path.join( path.dirname( __dirname ), 'scripts' ) ) + .filter( ( f ) => path.extname( f ) === '.js' ) + .map( ( f ) => path.basename( f, '.js' ) ); + module.exports = { fromProjectRoot, fromConfigRoot, fromScriptsRoot, + getScripts, hasProjectFile, hasScriptFile, }; diff --git a/packages/scripts/utils/index.js b/packages/scripts/utils/index.js index 0629a0e21bcab..3174d02dad5b6 100644 --- a/packages/scripts/utils/index.js +++ b/packages/scripts/utils/index.js @@ -5,6 +5,7 @@ const { getArgFromCLI, getArgsFromCLI, getFileArgsFromCLI, + getNodeArgsFromCLI, hasArgInCLI, hasFileArgInCLI, spawnScript, @@ -32,6 +33,7 @@ module.exports = { getArgFromCLI, getArgsFromCLI, getFileArgsFromCLI, + getNodeArgsFromCLI, getWebpackArgs, hasBabelConfig, hasArgInCLI, diff --git a/packages/scripts/utils/test/index.js b/packages/scripts/utils/test/index.js index 45c2e27339e35..218e9cc1bc82d 100644 --- a/packages/scripts/utils/test/index.js +++ b/packages/scripts/utils/test/index.js @@ -121,6 +121,32 @@ describe( 'utils', () => { expect( console ).toHaveLogged(); } ); + test( 'should pass inspect args to node', () => { + crossSpawnMock.mockReturnValueOnce( { status: 0 } ); + + expect( () => + spawnScript( scriptName, [], [ '--inspect-brk' ] ) + ).toThrow( 'Exit code: 0.' ); + expect( crossSpawnMock ).toHaveBeenCalledWith( + 'node', + [ '--inspect-brk', expect.stringContaining( scriptName ) ], + { stdio: 'inherit' } + ); + } ); + + test( 'should pass script args to the script', () => { + crossSpawnMock.mockReturnValueOnce( { status: 0 } ); + + expect( () => + spawnScript( scriptName, [ '--runInBand' ] ) + ).toThrow( 'Exit code: 0.' ); + expect( crossSpawnMock ).toHaveBeenCalledWith( + 'node', + [ expect.stringContaining( scriptName ), '--runInBand' ], + { stdio: 'inherit' } + ); + } ); + test( 'should finish successfully when the script properly executed', () => { crossSpawnMock.mockReturnValueOnce( { status: 0 } );