From 6c937e161ffaff850fb0bd472fa11bbb02e9cbe4 Mon Sep 17 00:00:00 2001 From: Jamon Holmgren Date: Wed, 15 May 2019 12:18:42 -0700 Subject: [PATCH] fix(plugins): Updates plugin generator and adds integration test (#1421 by @jamonholmgren) Fixes #1418. --- docs/advanced-guides/creating-generators.md | 6 +- docs/advanced-guides/creating-plugins.md | 20 +----- package.json | 2 +- src/commands/plugin.ts | 37 ++++------- .../ignite/add-plugin-screen-examples.ts | 1 - src/lib/validate-name.ts | 2 + .../plugin/boilerplate/Tests/AppTest.js | 1 - .../boilerplate/{App/App.js => app/app.js} | 0 .../plugin/boilerplate/index.js.ejs.ejs | 4 +- .../plugin/boilerplate/tests/app.test.js | 5 ++ src/templates/plugin/commands/thing.js.ejs | 55 +++++++++------- src/templates/plugin/gitignore | 1 + src/templates/plugin/package.json.ejs | 15 ++--- src/templates/plugin/plugin.js.ejs | 40 ++++++------ src/templates/plugin/templates/Example.js.ejs | 10 --- .../plugin/templates/thing.js.ejs.ejs | 2 - src/templates/plugin/test/add.js.ejs | 16 ++--- src/templates/plugin/test/remove.js.ejs | 8 +-- .../ignite-plugin/plugin-new.test.js | 62 +++++++++++++++++++ 19 files changed, 156 insertions(+), 131 deletions(-) delete mode 100644 src/templates/plugin/boilerplate/Tests/AppTest.js rename src/templates/plugin/boilerplate/{App/App.js => app/app.js} (100%) create mode 100644 src/templates/plugin/boilerplate/tests/app.test.js delete mode 100644 src/templates/plugin/templates/Example.js.ejs create mode 100644 tests/integration/ignite-plugin/plugin-new.test.js diff --git a/docs/advanced-guides/creating-generators.md b/docs/advanced-guides/creating-generators.md index 7b5868644..8229d539a 100644 --- a/docs/advanced-guides/creating-generators.md +++ b/docs/advanced-guides/creating-generators.md @@ -33,10 +33,10 @@ By answering `yes` to the generator question, you will be given the files necess ├── index.js ├── package.json └── templates - └── MyGeneratorExample.js + └── my-generator-example.js ``` -`commands/generate/example.js` exports a function whose responsibility is to queue a job to copy the template to the project. `templates/MyGeneratorExample.js` (or `{generator name}Example.js`) is the template that will be copied over. Rename or copy these files appropriately for your generator. The other files are standard plugin files; see the plugin guide to review plugins. +`commands/generate/example.js` exports a function whose responsibility is to queue a job to copy the template to the project. `templates/my-generator-example.js` (or `{generator name}-example.js`) is the template that will be copied over. Rename or copy these files appropriately for your generator. The other files are standard plugin files; see the plugin guide to review plugins. In the example `commands/generate/example.js`, you can see that the generator function accepts a `toolbox` parameter. You can use this to prompt the user to select options from a list. The following is an example of this from the ListView generator. @@ -58,7 +58,7 @@ This example uses the resulting `type` to select a template from several availab ```javascript // commands/listview.js const componentTemplate = type === 'With Sections' ? 'listview-sections' : 'listview' -const jobs = [{ template: `${componentTemplate}.ejs`, target: `App/Containers/${name}.js` }] +const jobs = [{ template: `${componentTemplate}.ejs`, target: `app/components/${name}.js` }] // Job handler await generate({ diff --git a/docs/advanced-guides/creating-plugins.md b/docs/advanced-guides/creating-plugins.md index 9a5ac7796..7baa8c387 100644 --- a/docs/advanced-guides/creating-plugins.md +++ b/docs/advanced-guides/creating-plugins.md @@ -6,7 +6,6 @@ We will be using https://github.com/ArnaudRinquin/react-native-radio-buttons as ### Generate a basic plugin structure - Run the provided plugin generator. Ignite CLI will automatically prepend your package name with `ignite-`. ``` @@ -43,27 +42,13 @@ The `plugin.js` file is the entrypoint for your plugin and provides the add/remo ### Add content to the example template -`templates/RadioButtonsExample.js.ejs` +`templates/radio-buttons-example.js.ejs` ``` import React from 'react' import { View, Text, TouchableWithoutFeedback } from 'react-native' -import ExamplesRegistry from '../../../App/Services/ExamplesRegistry' import { RadioButtons } from 'react-native-radio-buttons' -// Example -ExamplesRegistry.addPluginExample('RadioButtons', () => - - - -) - const options = [ "Option 1", "Option 2" @@ -89,7 +74,6 @@ const renderOption = (option, selected, onSelect, index) => { const renderContainer = (optionNodes) => { return {optionNodes} } - ``` ### Add the plugin to the Ignite application @@ -98,7 +82,7 @@ const renderContainer = (optionNodes) => { ignite add radio-buttons ``` -NOTE: If your plugin is not on npm yet, Make sure you have `IGNITE_PLUGIN_PATH` set as an ENV variable in your shell profile. It should point to the directory that contains the plugin you are writing. +NOTE: If your plugin is not on npm yet, make sure you have `IGNITE_PLUGIN_PATH` set as an ENV variable in your shell profile. It should point to the directory that contains the plugin you are writing. ``` ~/.bash_profile diff --git a/package.json b/package.json index 314de9396..8be3f8c94 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test": "jest tests/fast", "watch": "jest tests/fast --watch", "lint": "tslint -p .", - "integration": "jest tests/integration", + "integration": "jest --runInBand tests/integration", "ci:test": "yarn test && yarn integration", "ci:publish": "yarn build && yarn semantic-release", "semantic-release": "semantic-release" diff --git a/src/commands/plugin.ts b/src/commands/plugin.ts index 0ce26ffe7..70074ef59 100644 --- a/src/commands/plugin.ts +++ b/src/commands/plugin.ts @@ -9,8 +9,8 @@ import { IgniteToolbox, IgniteCopyJob } from '../types' * @returns {Object} The answers. */ const walkthrough = async (toolbox: IgniteToolbox) => { - const minOptions = { template: 'No', command: 'No' } - const maxOptions = { template: 'Yes', command: 'Yes' } + const minOptions = { boilerplate: 'No', generator: 'No' } + const maxOptions = { boilerplate: 'Yes', generator: 'Yes' } if (toolbox.parameters.options.min) { return minOptions } @@ -27,13 +27,7 @@ const walkthrough = async (toolbox: IgniteToolbox) => { choices: ['No', 'Yes'], }, { - name: 'template', - message: 'Will your plugin have an example component?', - type: 'list', - choices: ['No', 'Yes'], - }, - { - name: 'command', + name: 'generator', message: 'Will your plugin have a generator command? (e.g. ignite generate )', type: 'list', choices: ['No', 'Yes'], @@ -66,30 +60,23 @@ const createNewPlugin = async (toolbox: IgniteToolbox) => { { template: 'plugin/test/add.js.ejs', target: `${pluginName}/test/add.js` }, { template: 'plugin/test/remove.js.ejs', target: `${pluginName}/test/remove.js` }, { template: 'plugin/test/interface.js.ejs', target: `${pluginName}/test/interface.js` }, - answers.template === 'Yes' && { - template: 'plugin/templates/Example.js.ejs', - target: `${pluginName}/templates/${name}Example.js.ejs`, - }, - answers.command === 'Yes' && { + // generator command template example + answers.generator === 'Yes' && { template: 'plugin/commands/thing.js.ejs', - target: `${pluginName}/commands/thing.js`, + target: `${pluginName}/commands/generate/${pluginName}.js`, }, - answers.command === 'Yes' && { + answers.generator === 'Yes' && { template: 'plugin/templates/thing.js.ejs.ejs', - target: `${pluginName}/templates/thing.js.ejs`, + target: `${pluginName}/templates/${pluginName}.js.ejs`, }, ] if (answers.boilerplate === 'Yes') { copyJobs.push({ template: 'plugin/boilerplate.js.ejs', target: `${pluginName}/boilerplate.js` }) copyJobs.push({ template: 'plugin/boilerplate/index.js.ejs.ejs', target: `${pluginName}/boilerplate/index.js.ejs` }) - copyJobs.push({ template: 'plugin/boilerplate/App/App.js', target: `${pluginName}/boilerplate/App/App.js` }) - copyJobs.push({ - template: 'plugin/boilerplate/Tests/AppTest.js', - target: `${pluginName}/boilerplate/Tests/AppTest.js`, - }) + copyJobs.push({ template: 'plugin/boilerplate/app/app.js', target: `${pluginName}/boilerplate/app/app.js` }) copyJobs.push({ - template: 'plugin/boilerplate/ignite/ignite.json', - target: `${pluginName}/boilerplate/ignite/ignite.json`, + template: 'plugin/boilerplate/tests/app.test.js', + target: `${pluginName}/boilerplate/tests/app.test.js`, }) } @@ -99,7 +86,7 @@ const createNewPlugin = async (toolbox: IgniteToolbox) => { pluginName, answers, igniteVersion: meta.version(), - isGenerator: answers.command === 'Yes', + isGenerator: answers.generator === 'Yes', }) } diff --git a/src/extensions/ignite/add-plugin-screen-examples.ts b/src/extensions/ignite/add-plugin-screen-examples.ts index bd4fe14a8..d4e636e39 100644 --- a/src/extensions/ignite/add-plugin-screen-examples.ts +++ b/src/extensions/ignite/add-plugin-screen-examples.ts @@ -65,7 +65,6 @@ export default (toolbox: IgniteToolbox) => { map(file => { // turn things like "examples/This File-Example.js" into "ThisFileExample" // for decent component names - // TODO: check for collisions in the future const exampleFileName = takeLast(1, split(path.sep, file.screen))[0] const componentName = replace(/.js|\s|-/g, '', exampleFileName) diff --git a/src/lib/validate-name.ts b/src/lib/validate-name.ts index f5457c4db..e2c6b3ff8 100644 --- a/src/lib/validate-name.ts +++ b/src/lib/validate-name.ts @@ -9,6 +9,8 @@ import { IgniteToolbox } from '../types' export default (pluginName: string, toolbox: IgniteToolbox): string => { const { strings, print } = toolbox + pluginName = pluginName.toLowerCase() + if (strings.isBlank(pluginName)) { print.info(`ignite plugin new ignite-foo\n`) print.error('Plugin name is required') diff --git a/src/templates/plugin/boilerplate/Tests/AppTest.js b/src/templates/plugin/boilerplate/Tests/AppTest.js deleted file mode 100644 index 8f7719304..000000000 --- a/src/templates/plugin/boilerplate/Tests/AppTest.js +++ /dev/null @@ -1 +0,0 @@ -// Example test file diff --git a/src/templates/plugin/boilerplate/App/App.js b/src/templates/plugin/boilerplate/app/app.js similarity index 100% rename from src/templates/plugin/boilerplate/App/App.js rename to src/templates/plugin/boilerplate/app/app.js diff --git a/src/templates/plugin/boilerplate/index.js.ejs.ejs b/src/templates/plugin/boilerplate/index.js.ejs.ejs index cbff19d18..a4415229a 100644 --- a/src/templates/plugin/boilerplate/index.js.ejs.ejs +++ b/src/templates/plugin/boilerplate/index.js.ejs.ejs @@ -14,7 +14,7 @@ */ import { AppRegistry } from 'react-native' -import App from './App/App.js' +import App from './app/app.js' -AppRegistry.registerComponent('<%- '<' + '%= props.name %' + '>' %>', () => App) +AppRegistry.registerComponent('<%- "<" + "%= props.name %" + ">" %>', () => App) diff --git a/src/templates/plugin/boilerplate/tests/app.test.js b/src/templates/plugin/boilerplate/tests/app.test.js new file mode 100644 index 000000000..e2c460742 --- /dev/null +++ b/src/templates/plugin/boilerplate/tests/app.test.js @@ -0,0 +1,5 @@ +const test = require('ava') + +test('sample test', async t => { + t.true(true) +}) diff --git a/src/templates/plugin/commands/thing.js.ejs b/src/templates/plugin/commands/thing.js.ejs index 5b64d6f4e..9c2aa345e 100644 --- a/src/templates/plugin/commands/thing.js.ejs +++ b/src/templates/plugin/commands/thing.js.ejs @@ -1,28 +1,37 @@ -// @cliDescription Example <%= props.name %> command -// Generates a "thing" (rename this to whatever -- component, model, anything). +/** + * This is an example Ignite plugin generator. You can run it when it's installed to + * your project by doing `ignite generate <%= props.name %> foo`. + * + * You can rename this command to anything you'd like, or add others. + * + * For more information on plugins, check out https://github.com/infinitered/gluegun/blob/master/docs/plugins.md. + */ -module.exports = async function (context) { - // Learn more about context: https://infinitered.github.io/gluegun/#/context-api.md - const { parameters, strings, print, ignite } = context - const { pascalCase, isBlank } = strings +module.exports = { + description: "Example <%= props.name %> generator", + run: async function (toolbox) { + // Learn more about toolbox: https://infinitered.github.io/gluegun/#/toolbox-api.md + const { parameters, strings, print, ignite } = toolbox + const { pascalCase, isBlank } = strings - // validation - if (isBlank(parameters.first)) { - print.info(`ignite generate thing \n`) - print.info('A name is required.') - return - } + // validation + if (isBlank(parameters.first)) { + print.info(`ignite generate <%= props.name %> \n`) + print.info('A name is required.') + return + } - const name = pascalCase(parameters.first) - const props = { name } + const name = pascalCase(parameters.first) + const props = { name } - // Copies the `thing.js.ejs` in your plugin's templates folder - // into App/Things/${name}.js. - const jobs = [{ - template: 'thing.js.ejs', - target: `App/Things/${name}.js` - }] + // Copies the `<%= props.name %>.js.ejs` in your plugin's templates folder + // into App/Things/${name}.js. + const jobs = [{ + template: '<%= props.name %>.js.ejs', + target: `app/${name}.js` + }] - // make the templates and pass in props with the third argument here - await ignite.copyBatch(context, jobs, props) -} + // make the templates and pass in props with the third argument here + await ignite.copyBatch(toolbox, jobs, props) + } +} \ No newline at end of file diff --git a/src/templates/plugin/gitignore b/src/templates/plugin/gitignore index b4aaa4f21..0fe6ccbcd 100644 --- a/src/templates/plugin/gitignore +++ b/src/templates/plugin/gitignore @@ -1,3 +1,4 @@ node_modules npm-debug.log generated +.vscode diff --git a/src/templates/plugin/package.json.ejs b/src/templates/plugin/package.json.ejs index 3db5cfa8d..6f12852c1 100644 --- a/src/templates/plugin/package.json.ejs +++ b/src/templates/plugin/package.json.ejs @@ -10,23 +10,24 @@ }, "scripts": { "lint": "standard", + "format": "prettier --write \"{**/*.ts,**/*.js}\" --loglevel error && standard --fix", "test": "ava", "watch": "ava --watch", - "coverage": "nyc ava", "shipit": "np" }, + "prettier": {}, "standard": { "parser": "babel-eslint" }, "url": "https://example.com/path/to/your/plugin", "repository": "githuborg/reponame", "devDependencies": { - "ava": "^0.18.2", - "babel-eslint": "^7.1.1", - "np": "^2.12.0", - "nyc": "^10.1.2", - "sinon": "^1.17.7", - "standard": "^8.6.0" + "ava": "^1.4.1", + "babel-eslint": "^10.0.1", + "np": "^5.0.1", + "sinon": "^7.3.2", + "standard": "^12.0.1", + "prettier": "^1.17.1" } } diff --git a/src/templates/plugin/plugin.js.ejs b/src/templates/plugin/plugin.js.ejs index 372c4a67f..adcffe1f8 100644 --- a/src/templates/plugin/plugin.js.ejs +++ b/src/templates/plugin/plugin.js.ejs @@ -6,25 +6,23 @@ const NPM_MODULE_VERSION = '0.0.1' // const PLUGIN_PATH = __dirname // const APP_PATH = process.cwd() -<% if (props.answers.template === 'Yes') { %>const EXAMPLE_FILE = '<%= props.name %>Example.js.ejs'<% } %> +<% if (props.answers.template === 'Yes') { %>const EXAMPLE_FILE = '<%= props.name %>.js.ejs'<% } %> -const add = async function (context) { - // Learn more about context: https://infinitered.github.io/gluegun/#/context-api.md - const { ignite, filesystem } = context +const add = async function (toolbox) { + // Learn more about toolbox: https://infinitered.github.io/gluegun/#/toolbox-api.md + const { ignite } = toolbox // install an NPM module and link it await ignite.addModule(NPM_MODULE_NAME, { link: true, version: NPM_MODULE_VERSION }) - <% if (props.answers.template === 'Yes') { %>await ignite.addPluginComponentExample(EXAMPLE_FILE, { title: '<%= props.name %> Example' })<% } %> - - // Example of copying templates/<%= props.name %> to App/<%= props.name %> - // if (!filesystem.exists(`${APP_PATH}/App/<%= props.name %>`)) { - // filesystem.copy(`${PLUGIN_PATH}/templates/<%= props.name %>`, `${APP_PATH}/App/<%= props.name %>`) + // Example of copying templates/<%= props.name %> to app/<%= props.pluginName %> + // if (!toolbox.filesystem.exists(`${APP_PATH}/app/<%= props.pluginName %>`)) { + // toolbox.filesystem.copy(`${PLUGIN_PATH}/templates/<%= props.pluginName %>`, `${APP_PATH}/app/<%= props.pluginName %>`) // } // Example of patching a file - // ignite.patchInFile(`${APP_PATH}/App/Config/AppConfig.js`, { - // insert: `import '../<%= props.name %>/<%= props.name %>'\n`, + // ignite.patchInFile(`${APP_PATH}/app/config/app-config.js`, { + // insert: `import '../<%= props.pluginName %>/<%= props.pluginName %>'\n`, // before: `export default {` // }) } @@ -32,24 +30,22 @@ const add = async function (context) { /** * Remove yourself from the project. */ -const remove = async function (context) { - // Learn more about context: https://infinitered.github.io/gluegun/#/context-api.md - const { ignite, filesystem } = context +const remove = async function (toolbox) { + // Learn more about toolbox: https://infinitered.github.io/gluegun/#/toolbox-api.md + const { ignite } = toolbox // remove the npm module and unlink it await ignite.removeModule(NPM_MODULE_NAME, { unlink: true }) - <% if (props.answers.template === 'Yes') { %>await ignite.removePluginComponentExample(EXAMPLE_FILE)<% } %> - - // Example of removing App/<%= props.name %> folder - // const remove<%= props.name %> = await context.prompt.confirm( - // 'Do you want to remove App/<%= props.name %>?' + // Example of removing app/<%= props.name %> folder + // const remove<%= props.pluginName %> = await toolbox.prompt.confirm( + // 'Do you want to remove app/<%= props.pluginName %>?' // ) - // if (remove<%= props.name %>) { filesystem.remove(`${APP_PATH}/App/<%= props.name %>`) } + // if (remove<%= props.pluginName %>) { toolbox.filesystem.remove(`${APP_PATH}/app/<%= props.pluginName %>`) } // Example of unpatching a file - // ignite.patchInFile(`${APP_PATH}/App/Config/AppConfig.js`, { - // delete: `import '../<%= props.name %>/<%= props.name %>'\n` + // ignite.patchInFile(`${APP_PATH}/app/config/app-config.js`, { + // delete: `import '../<%= props.pluginName %>/<%= props.pluginName %>'\n` // ) } diff --git a/src/templates/plugin/templates/Example.js.ejs b/src/templates/plugin/templates/Example.js.ejs deleted file mode 100644 index e2d07697c..000000000 --- a/src/templates/plugin/templates/Example.js.ejs +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { View, Text } from 'react-native' -import ExamplesRegistry from '../../../App/Services/ExamplesRegistry' - -// Example -ExamplesRegistry.addPluginExample('<%= props.name %>', () => - - {/* Add your component or plugin example here */} - -) diff --git a/src/templates/plugin/templates/thing.js.ejs.ejs b/src/templates/plugin/templates/thing.js.ejs.ejs index f464db8fe..af31dc215 100644 --- a/src/templates/plugin/templates/thing.js.ejs.ejs +++ b/src/templates/plugin/templates/thing.js.ejs.ejs @@ -2,7 +2,5 @@ // Pass in props from your command. <%- - // i'm so sorry - the right answer is in - // https://github.com/mde/ejs/blob/master/docs/syntax.md somewhere '<' + '%= props.name %' + '>' %> diff --git a/src/templates/plugin/test/add.js.ejs b/src/templates/plugin/test/add.js.ejs index 46b372c83..9251060cd 100644 --- a/src/templates/plugin/test/add.js.ejs +++ b/src/templates/plugin/test/add.js.ejs @@ -5,19 +5,13 @@ const plugin = require('../plugin') test('adds the proper npm module and component example', async t => { // spy on few things so we know they're called const addModule = sinon.spy() - const addPluginComponentExample = sinon.spy() - // mock a context - const context = { - ignite: { addModule, addPluginComponentExample } + // mock an Ignite toolbox + const toolbox = { + ignite: { addModule } } - await plugin.add(context) + await plugin.add(toolbox) - t.true(addModule.calledWith('react-native-MODULENAME', { link: true })) - t.true( - addPluginComponentExample.calledWith('<%= props.name %>Example.js', { - title: '<%= props.name %> Example' - }) - ) + t.true(addModule.calledWith('react-native-MODULENAME', { link: true, version: '0.0.1' })) }) diff --git a/src/templates/plugin/test/remove.js.ejs b/src/templates/plugin/test/remove.js.ejs index 4531dfdf1..edfa02c11 100644 --- a/src/templates/plugin/test/remove.js.ejs +++ b/src/templates/plugin/test/remove.js.ejs @@ -4,14 +4,12 @@ const plugin = require('../plugin') test('removes <%= props.name %>', async t => { const removeModule = sinon.spy() - const removePluginComponentExample = sinon.spy() - const context = { - ignite: { removeModule, removePluginComponentExample } + const toolbox = { + ignite: { removeModule } } - await plugin.remove(context) + await plugin.remove(toolbox) t.true(removeModule.calledWith('react-native-MODULENAME', { unlink: true })) - t.true(removePluginComponentExample.calledWith('<%= props.name %>Example.js')) }) diff --git a/tests/integration/ignite-plugin/plugin-new.test.js b/tests/integration/ignite-plugin/plugin-new.test.js new file mode 100644 index 000000000..df13d8f17 --- /dev/null +++ b/tests/integration/ignite-plugin/plugin-new.test.js @@ -0,0 +1,62 @@ +const { system, filesystem } = require('gluegun') +const tempy = require('tempy') +const stripANSI = require('strip-ansi') + +const IGNITE = 'node ' + filesystem.path(`${__dirname}/../../../bin/ignite`) + +const PLUGIN_NAME = 'foo' + +jest.setTimeout(10 * 60 * 1000) + +const originalDir = process.cwd() +const opts = { stdio: 'inherit' } + +beforeEach(() => { + const tempDir = tempy.directory() + process.chdir(tempDir) +}) + +afterEach(() => { + process.chdir(originalDir) +}) + +test('spins up an Ignite plugin with min options and performs various checks', async done => { + const resultANSI = await system.run(`${IGNITE} plugin new ${PLUGIN_NAME} --min`, opts) + const result = stripANSI(resultANSI) + + expect(result).toContain(`Creating new plugin: ignite-${PLUGIN_NAME}`) + + process.chdir(`./ignite-${PLUGIN_NAME}`) + const dirs = filesystem.subdirectories('.') + expect(dirs).toContain('test') + expect(dirs).not.toContain('boilerplate') + expect(dirs).not.toContain('commands') + expect(dirs).not.toContain('templates') + expect(filesystem.exists('package.json')).toBeTruthy() + expect(filesystem.exists('plugin.js')).toBeTruthy() + + done() +}) + +test('spins up an Ignite plugin with max options and performs various checks', async done => { + const resultANSI = await system.run(`${IGNITE} plugin new ${PLUGIN_NAME} --max`, opts) + const result = stripANSI(resultANSI) + + expect(result).toContain(`Creating new plugin: ignite-${PLUGIN_NAME}`) + + process.chdir(`./ignite-${PLUGIN_NAME}`) + const dirs = filesystem.subdirectories('.') + expect(dirs).toContain('test') + expect(dirs).toContain('boilerplate') + expect(dirs).toContain('commands') + expect(dirs).toContain('templates') + expect(filesystem.exists('package.json')).toBeTruthy() + expect(filesystem.exists('plugin.js')).toBeTruthy() + + await system.run(`yarn`) + await system.run(`yarn format`) + const testResults = await system.run(`yarn test`) + expect(testResults).toContain(`4 tests passed`) + + done() +})