Repository: Raathigesh/majestic Branch: master Commit: 2bb004718872 Files: 120 Total size: 139.4 KB Directory structure: gitextract_z09lw4cd/ ├── .all-contributorsrc ├── .babelrc ├── .github/ │ ├── issue_template.md │ ├── stale.yml │ └── workflows/ │ └── nodejs.yml ├── .gitignore ├── .prettierrc ├── .vscode/ │ └── launch.json ├── CONTRIBUTING.MD ├── LICENSE ├── README.md ├── Troubleshooting.md ├── branding/ │ ├── Github Readme Banner.psd │ └── small-logo.psd ├── integration/ │ ├── cypress/ │ │ ├── fixtures/ │ │ │ └── example.json │ │ ├── integration/ │ │ │ └── basic/ │ │ │ └── basic-functionality.js │ │ ├── plugins/ │ │ │ └── index.js │ │ └── support/ │ │ ├── commands.js │ │ └── index.js │ ├── cypress.json │ ├── kill.js │ ├── package.json │ └── projects/ │ └── basic/ │ ├── __snapshots__/ │ │ └── test-snapshot-failure.spec.js.snap │ ├── app.js │ ├── babel.config.js │ ├── package.json │ ├── test-all-good.spec.js │ ├── test-few-failure.spec.js │ ├── test-only.spec.js │ ├── test-snapshot-failure.spec.js │ └── test-snapshot-text.spec.js ├── nodemon.json ├── package.json ├── scripts/ │ ├── webpack.server.config.js │ └── webpack.ui.config.js ├── server/ │ ├── api/ │ │ ├── app/ │ │ │ ├── app.ts │ │ │ └── resolver.ts │ │ ├── index.ts │ │ ├── runner/ │ │ │ ├── resolver.ts │ │ │ ├── status.ts │ │ │ └── type.ts │ │ └── workspace/ │ │ ├── coverage.ts │ │ ├── resolver.ts │ │ ├── summary.ts │ │ ├── test-file.ts │ │ ├── test-item.ts │ │ ├── test-result/ │ │ │ ├── console-log.ts │ │ │ ├── file-result.ts │ │ │ └── test-item-result.ts │ │ ├── tree.ts │ │ └── workspace.ts │ ├── event-emitter/ │ │ └── index.ts │ ├── index.ts │ ├── logger.ts │ ├── services/ │ │ ├── ast/ │ │ │ ├── inspector.ts │ │ │ └── parser.ts │ │ ├── cli.ts │ │ ├── config-resolver.ts │ │ ├── file-watcher/ │ │ │ └── index.ts │ │ ├── jest-manager/ │ │ │ ├── cli-args.ts │ │ │ ├── index.ts │ │ │ └── scripts/ │ │ │ ├── patch.js │ │ │ └── reporter.js │ │ ├── project.ts │ │ ├── result-handler-api.ts │ │ ├── results.ts │ │ └── types.ts │ ├── static-files.ts │ └── typings.d.ts ├── tsconfig.json ├── tsconfig.server.json └── ui/ ├── apollo-client.ts ├── app.gql ├── app.tsx ├── components/ │ └── button.tsx ├── container.tsx ├── coverage-panel/ │ └── index.tsx ├── error.tsx ├── hooks/ │ └── use-keys.ts ├── index.tsx ├── loading.tsx ├── query.gql ├── runner-status-query.gql ├── runner-status-subs.gql ├── search/ │ └── index.tsx ├── set-selected-file.gql ├── sidebar/ │ ├── execution-indicator.tsx │ ├── file-item.tsx │ ├── index.tsx │ ├── logo.tsx │ ├── run.gql │ ├── set-collect-coverage.gql │ ├── set-watch-mode.gql │ ├── should-collect-coverage.gql │ ├── summary/ │ │ └── index.tsx │ ├── transformer.ts │ └── tree.tsx ├── split-panel-style.ts ├── stop-runner.gql ├── summary-query.gql ├── summary-subscription.gql ├── test-file/ │ ├── console-panel/ │ │ └── index.tsx │ ├── error-panel/ │ │ └── index.tsx │ ├── file-items-subscription.gql │ ├── index.tsx │ ├── open-failure.gql │ ├── query.gql │ ├── result.gql │ ├── run-file.gql │ ├── subscription.gql │ ├── summary/ │ │ ├── index.tsx │ │ ├── open-in-editor.gql │ │ └── open-snap-in-editor.gql │ ├── test-indicator.tsx │ ├── test-item.tsx │ ├── transformer.ts │ ├── update-snapshot.gql │ └── use-subscription.tsx ├── theme.ts └── typings.d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "files": [ "README.md" ], "imageSize": 100, "commit": false, "contributors": [ { "login": "duncanbeevers", "name": "Duncan Beevers", "avatar_url": "https://avatars0.githubusercontent.com/u/7367?v=4", "profile": "http://www.duncanbeevers.com", "contributions": [ "code" ] }, { "login": "M4cs", "name": "Max Bridgland", "avatar_url": "https://avatars3.githubusercontent.com/u/34947910?v=4", "profile": "https://github.com/M4cs", "contributions": [ "doc", "ideas", "bug", "code" ] }, { "login": "yurm04", "name": "Yuraima Estevez", "avatar_url": "https://avatars0.githubusercontent.com/u/4642404?v=4", "profile": "https://github.com/yurm04", "contributions": [ "code" ] }, { "login": "jake-nz", "name": "Jake Crosby", "avatar_url": "https://avatars2.githubusercontent.com/u/437471?v=4", "profile": "http://jake.nz", "contributions": [ "code" ] }, { "login": "gavinhenderson", "name": "Gavin Henderson", "avatar_url": "https://avatars1.githubusercontent.com/u/1359202?v=4", "profile": "http://gavinhenderson.me", "contributions": [ "code" ] }, { "login": "briwa", "name": "briwa", "avatar_url": "https://avatars1.githubusercontent.com/u/8046636?v=4", "profile": "https://briwa.github.io", "contributions": [ "code" ] }, { "login": "Luanf", "name": "Luan Ferreira", "avatar_url": "https://avatars0.githubusercontent.com/u/9099705?v=4", "profile": "https://github.com/Luanf", "contributions": [ "code" ] }, { "login": "cse-tushar", "name": "Tushar Gupta", "avatar_url": "https://avatars3.githubusercontent.com/u/12570521?v=4", "profile": "https://github.com/cse-tushar", "contributions": [ "code" ] }, { "login": "agustif", "name": "Agusti Fernandez", "avatar_url": "https://avatars3.githubusercontent.com/u/6601142?v=4", "profile": "https://agu.st/", "contributions": [ "code", "ideas" ] }, { "login": "moos", "name": "Moos", "avatar_url": "https://avatars2.githubusercontent.com/u/233047?v=4", "profile": "http://blog.42at.com", "contributions": [ "bug", "code", "doc" ] }, { "login": "MacZel", "name": "MacZel", "avatar_url": "https://avatars3.githubusercontent.com/u/25805810?v=4", "profile": "http://maciejzelek.space", "contributions": [ "code", "ideas" ] }, { "login": "krazylegz", "name": "Vikram Dighe", "avatar_url": "https://avatars2.githubusercontent.com/u/36250?v=4", "profile": "https://github.com/krazylegz", "contributions": [ "code" ] }, { "login": "jsmey", "name": "John Smey", "avatar_url": "https://avatars2.githubusercontent.com/u/10177710?v=4", "profile": "https://github.com/jsmey", "contributions": [ "code", "ideas", "bug" ] }, { "login": "BuckAMayzing", "name": "BuckAMayzing", "avatar_url": "https://avatars2.githubusercontent.com/u/19292614?v=4", "profile": "https://github.com/BuckAMayzing", "contributions": [ "code", "bug" ] }, { "login": "rahulakrishna", "name": "Rahul A. Krishna", "avatar_url": "https://avatars2.githubusercontent.com/u/10240002?v=4", "profile": "http://rahulakrishna.github.io", "contributions": [ "code", "ideas", "tool" ] }, { "login": "amilajack", "name": "Amila Welihinda", "avatar_url": "https://avatars1.githubusercontent.com/u/6374832?v=4", "profile": "https://amilajack.com", "contributions": [ "infra" ] }, { "login": "gregveres", "name": "gregveres", "avatar_url": "https://avatars2.githubusercontent.com/u/12899823?v=4", "profile": "https://github.com/gregveres", "contributions": [ "bug", "code" ] }, { "login": "adamkleingit", "name": "adam klein", "avatar_url": "https://avatars3.githubusercontent.com/u/889418?v=4", "profile": "http://adamklein.dev", "contributions": [ "test", "code" ] }, { "login": "rbarbazz", "name": "Raphaël Barbazza", "avatar_url": "https://avatars1.githubusercontent.com/u/42906704?v=4", "profile": "http://www.raphaelbarbazza.com", "contributions": [ "code" ] }, { "login": "philals", "name": "Phil Alsford", "avatar_url": "https://avatars3.githubusercontent.com/u/8849355?v=4", "profile": "https://philalsford.com", "contributions": [ "doc" ] } ], "contributorsPerLine": 7, "projectName": "majestic", "projectOwner": "Raathigesh", "repoType": "github", "repoHost": "https://github.com", "skipCi": true } ================================================ FILE: .babelrc ================================================ { "presets": [ "@babel/preset-react", [ "@babel/preset-typescript", { "isTSX": true, "allExtensions": true } ], "@babel/preset-env" ], "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-class-properties", { "loose": true }], "@babel/plugin-proposal-object-rest-spread", "babel-plugin-styled-components" ] } ================================================ FILE: .github/issue_template.md ================================================ ## Is this a bug report or a feature request? Please specify whether this is a feature request or a bug. ## Version Info - Version of Majestic: - Version of Jest: - Version of Node: - Operating System: ## Reproduction Repo If this is a bug report, please provide a minimal github repository where this mentioned issue is reproducible. Majestic makes certain assumptions regarding the test setup and it's very hard to guess the issue without looking at the exact configurations that you are using. ================================================ FILE: .github/stale.yml ================================================ daysUntilStale: 30 daysUntilClose: 7 onlyLabels: [🦄 Need more info] markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. closeComment: false ================================================ FILE: .github/workflows/nodejs.yml ================================================ name: Node CI on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [12.x] steps: - uses: actions/checkout@v1 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: npm install, build, and test run: | yarn install yarn prod yarn integration env: CI: true ================================================ FILE: .gitignore ================================================ node_modules dist ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "trailingComma": "all" } ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "runtimeArgs": ["-r", "./node_modules/ts-node/register"], "args": ["${workspaceFolder}/server/index.ts", "--noOpen"], "console": "integratedTerminal", "env": { "TS_NODE_PROJECT": "./tsconfig.server.json", "ROOT": "" } } ] } ================================================ FILE: CONTRIBUTING.MD ================================================ ### Preparing Majestic - Clone this repository - Install dependencies with `yarn install` ### Running Majestic Majestic has 2 main components as follows - The UI written in React JS and GraphQL - The UI source is in `./ui` - Running `yarn ui` will start the webpack dev server - A Node JS GraphQL server - The server source is in `./server` - Create a sample projector use one of your project with Jest so you can test your changes - If you are using VSCode, edit the `\.vscode\launch.json` file and change the `ROOT` directory to your sample project and then you can press `F5` to run the server. - If you are not using VSCode, edit the `\server\services\cli.ts` file and change the root path so you test with your sample project and then running `yarn watch-server` will start the server in watch mode ## Running integration test We have a couple of integration tests written using [Cypress](https://www.cypress.io/) available in the `./integration` folder. To run the integration test - Do a production build by running `yarn prod` - `cd ./integration` - Run `yarn prepare-packages` to install required packages - Run `yarn run-integration` to run the integration tests ### Building Production Bundle The UI is built by Webpack and the server is also built by Webpack to decrease install times. Run `yarn prod` to build a production bundle and the artifacts would be available in `dist` folder. ### Publishing a new release Running `yarn ship` will perform a production build and will publish a new version to npm. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Raathigeshan Kugarajan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================


Join the community on Spectrum

Majestic is a GUI for [Jest](https://jestjs.io/) - ✅ Run all the tests or a single file - ⏱ Toggle watch mode - 📸 Update snapshots - ❌ Examine test failures as they happen - ⏲ Console.log() to the UI for debugging - 🚔 Built-in coverage report - 🔍 Search tests - 💎 Works with flow and typescript projects - 📦 Works with Create react app > Majestic supports Jest 20 and above ### Get started Run majestic via `npx` in a project directory ```bash cd ./my-jest-project # go into a project with Jest npx majestic # execute majestic ``` or install Majestic globally via Yarn and run majestic ```bash yarn global add majestic # install majestic globally cd ./my-jest-project # go into a project with Jest majestic # execute majestic ``` or install Majestic globally via Npm and run majestic ```bash npm install majestic -g # install majestic globally cd ./my-jest-project # go into a project with Jest majestic # execute majestic ``` ### Running as an app Running with the `--app` flag will launch Majestic as a chrome app. ### Optional configuration You can configure Majestic by adding `majestic` key to `package.json`. ```javascript // package.json { "majestic": { // if majestic fails to find the Jest package, you can provide it here. Should be relative to the package.json "jestScriptPath": "../node_modules/jest/bin/jest.js", // if you want to pass additional arguments to Jest, do it here "args": ['--config=./path/to/config/file/jest.config.js'], // environment variables to pass to the process "env": { "CI": "true" } } } ``` #### Optional configuration in project with multiple Jest configuration files ```javascript { "majestic": { "jestScriptPath": "../node_modules/jest/bin/jest.js", "configs": { "config1": { "args": [], "env": {} }, "config2": { "args": [], "env": {} } } } } ``` ### Arguments `--config` - Will use this config from the list supplied in optional configuration. `--debug` - Will output extra debug info to console. Helps with debugging. `--noOpen` - Will prevent from automatically opening the UI url in the browser. `--port` - Will use this port if available, else Majestic will pick another free port. `--version` - Will print the version of Majestic and will exit. ### Shortcut keys `alt+t` - run all tests `alt+enter` - run selected file `alt+w` - watch `alt+s` - search `escape` - close search ### Troubleshooting Have a look at some of the [common workarounds](./Troubleshooting.md). ### Contribute Have a look at the [contribution guide](./CONTRIBUTING.MD). ## Contributors Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

Duncan Beevers

💻

Max Bridgland

📖 🤔 🐛 💻

Yuraima Estevez

💻

Jake Crosby

💻

Gavin Henderson

💻

briwa

💻

Luan Ferreira

💻

Tushar Gupta

💻

Agusti Fernandez

💻 🤔

Moos

🐛 💻 📖

MacZel

💻 🤔

Vikram Dighe

💻

John Smey

💻 🤔 🐛

BuckAMayzing

💻 🐛

Rahul A. Krishna

💻 🤔 🔧

Amila Welihinda

🚇

gregveres

🐛 💻

adam klein

⚠️ 💻

Raphaël Barbazza

💻

Phil Alsford

📖
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! ================================================ FILE: Troubleshooting.md ================================================ #### Custom react-scripts If you're using a custom [react-scripts](https://www.npmjs.com/package/react-scripts) in your CRA app, set `jestScriptPath` to your script path. e.g.: ``` "jestScriptPath": "./node_modules/my-react-scripts/scripts/test.js" ``` #### Absolute import paths Set `NODE_PATH` in the majestic env config: ``` "env": { "NODE_PATH": "./src" } ``` #### Mocked networks When using [nock](https://github.com/nock/nock) (or other mock proxies) and get an error: > (node:50245) UnhandledPromiseRejectionWarning: FetchError: request to http://localhost:4000/test-result failed, reason: Nock: Not allow net connect for "localhost:4000/test-result" make sure to re-enable net connection after the test completes, e.g. (in a setup file) : ``` beforeAll(() => { nock.disableNetConnect(); }); afterAll(() => { nock.enableNetConnect(); }); ``` ================================================ FILE: integration/cypress/fixtures/example.json ================================================ { "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" } ================================================ FILE: integration/cypress/integration/basic/basic-functionality.js ================================================ /// context('basic', () => { beforeEach(() => { cy.visit('http://localhost:9000', { timeout: 9000, }); }); after(() => { cy.exec('yarn kill-app'); }); it('should display passing test count', () => { cy.wait(2000); cy.getByText('test-all-good.spec.js').click({ force: true }); cy.getByText('Run').click(); cy.wait(5000); cy.queryByText('6 Passing tests').should('exist'); }); it('should display failure tests', () => { cy.wait(2000); cy.getByText('test-few-failure.spec.js').click({ force: true }); cy.wait(2000); cy.getByText('Run').click(); cy.wait(5000); cy.queryByText('5 Passing tests').should('exist'); }); it('should show update snapshot button', () => { cy.wait(2000); cy.getByText('test-snapshot-failure.spec.js').click({ force: true }); cy.wait(2000); cy.getByText('Run').click(); cy.wait(5000); cy.queryByText('Update Snapshot').should('exist'); }); it('should not show update snapshot button', () => { cy.wait(2000); cy.getByText('test-snapshot-text.spec.js').click({ force: true }); cy.wait(2000); cy.getByText('Run').click(); cy.wait(5000); cy.queryByText('Update Snapshot').should('not.exist'); }); }); ================================================ FILE: integration/cypress/plugins/index.js ================================================ // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config }; ================================================ FILE: integration/cypress/support/commands.js ================================================ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite // existing commands. // // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** // // // -- This is a parent command -- // Cypress.Commands.add("login", (email, password) => { ... }) // // // -- This is a child command -- // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) // // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) import 'cypress-testing-library/add-commands'; ================================================ FILE: integration/cypress/support/index.js ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') ================================================ FILE: integration/cypress.json ================================================ { "projectId": "q19erz" } ================================================ FILE: integration/kill.js ================================================ const fkill = require('fkill'); fkill(':9000', { force: true, tree: true, }) .then(() => { console.log('Killed process'); }) .catch(e => { console.log("Couldn't kill process: ", e); }); ================================================ FILE: integration/package.json ================================================ { "name": "integration", "version": "1.0.0", "main": "index.js", "license": "MIT", "scripts": { "prepare-packages": "yarn && cd ./projects/basic && yarn", "open-tests": "cypress open", "run-tests": "wait-on http://localhost:9000 && cypress run --record --key a7d33ff7-5893-4158-9ec2-f71b32138c8b", "kill-app": "node ./kill.js", "prepare-basic-app": "node ./kill.js && cd ./projects/basic && node ../../../dist/server/index.js --port=9000 --debug", "integration-app": "concurrently --success=last \"yarn prepare-basic-app\" \"yarn run-tests\"", "run-integration": "yarn integration-app", "run-in-ci": "yarn prepare-packages && yarn run-integration" }, "dependencies": { "concurrently": "^4.1.0", "cypress": "^3.2.0", "cypress-testing-library": "^2.3.6", "fkill": "^6.0.0", "wait-on": "^3.2.0" } } ================================================ FILE: integration/projects/basic/__snapshots__/test-snapshot-failure.spec.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`test Snapsh0t test 1`] = `
Hello world
`; ================================================ FILE: integration/projects/basic/app.js ================================================ import React, { Component } from 'react'; class App extends Component { render() { return
Hello world 123
; } } export default App; ================================================ FILE: integration/projects/basic/babel.config.js ================================================ module.exports = { presets: [ [ '@babel/preset-env', { targets: { node: 'current', }, }, ], '@babel/preset-react', ], }; ================================================ FILE: integration/projects/basic/package.json ================================================ { "name": "simple-jest", "version": "1.0.0", "main": "index.js", "license": "MIT", "scripts": { "test": "jest" }, "dependencies": { "@babel/preset-react": "^7.0.0", "jest": "^25.3.0", "react": "^16.8.4", "react-dom": "^16.8.4", "react-test-renderer": "^16.8.4" }, "devDependencies": { "@babel/core": "^7.3.4", "@babel/preset-env": "^7.3.4", "babel-jest": "^25.3.0" } } ================================================ FILE: integration/projects/basic/test-all-good.spec.js ================================================ describe('test', () => { it('should add third', () => { expect(5).toBe(5); }); it('should add 1', () => { expect(5).toBe(5); }); it('should add 2', () => { expect(5).toBe(5); }); it('should add 3', () => { expect(5).toBe(5); }); it('should add 4', () => { expect(5).toBe(5); }); it('should add 5', () => { expect(5).toBe(5); }); }); ================================================ FILE: integration/projects/basic/test-few-failure.spec.js ================================================ describe('test', () => { it('should add third', () => { expect(5).toBe(6); }); it('should add 1', () => { expect(5).toBe(5); }); it('should add 2', () => { expect(5).toBe(5); }); it('should add 3', () => { expect(5).toBe(5); }); it('should add 4', () => { expect(5).toBe(5); }); it('should add 5', () => { expect(5).toBe(5); }); }); ================================================ FILE: integration/projects/basic/test-only.spec.js ================================================ describe('describe', () => { it('it', () => {}); test('test', () => {}); }); describe.only('describe.only', () => { it('it', () => {}); test('test', () => {}); it.only('it.only', () => {}); test.only('test.only', () => {}); }); fdescribe('fdescribe', () => { it('it', () => {}); fit('fit', () => {}); }); ================================================ FILE: integration/projects/basic/test-snapshot-failure.spec.js ================================================ import renderer from 'react-test-renderer'; import React from 'react'; import App from './app'; describe('test', () => { it('Snapsh0t test', () => { // Make sure we don't use 'snapshot' because it fools the snapshot button const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); }); ================================================ FILE: integration/projects/basic/test-snapshot-text.spec.js ================================================ describe('test', () => { it('Should not show snapshot button', () => { expect('snapshot').toBe('a snapshot'); }); }); ================================================ FILE: nodemon.json ================================================ { "ignore": [".git", "node_modules"], "watch": ["server"], "exec": "ts-node --project ./tsconfig.server.json ./server/index.ts", "ext": "ts" } ================================================ FILE: package.json ================================================ { "name": "majestic", "version": "1.8.1", "engines": { "node": ">=7.10.1" }, "main": "index.js", "license": "MIT", "scripts": { "ui": "webpack-dev-server --env.development --config ./scripts/webpack.ui.config.js", "server": "ts-node --project ./tsconfig.server.json ./server/index.ts", "build-server": "cross-env BABEL_ENV='production' webpack --env.production --config ./scripts/webpack.server.config.js", "build-ui": "cross-env BABEL_ENV='production' rimraf dist && webpack --env.production --config ./scripts/webpack.ui.config.js", "prod": "npm run build-ui && npm run build-server", "watch-server": "nodemon", "ship": "npm run prod && np --yolo", "integration": "cd ./integration && yarn run-in-ci" }, "dependencies": { "node-fetch": "^2.3.0", "open": "^6.0.0", "read-pkg-up": "^4.0.0" }, "devDependencies": { "@babel/core": "^7.1.2", "@babel/parser": "^7.2.3", "@babel/plugin-proposal-class-properties": "^7.0.0", "@babel/plugin-proposal-decorators": "^7.1.2", "@babel/plugin-proposal-object-rest-spread": "^7.0.0", "@babel/plugin-proposal-optional-chaining": "^7.8.3", "@babel/polyfill": "^7.0.0", "@babel/preset-env": "^7.0.0", "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.0.0", "@babel/traverse": "^7.2.3", "@types/babel-traverse": "^6.25.4", "@types/chokidar": "^1.7.5", "@types/express": "^4.16.0", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-lib-source-maps": "^1.2.1", "@types/react": "^16.8.6", "@types/react-dom": "^16.8.2", "@types/react-split-pane": "^0.1.67", "@types/styled-components": "^4.1.4", "@types/styled-system": "^3.1.0", "ansi-to-html": "^0.6.10", "apollo-client": "^2.3.8", "apollo-client-preset": "^1.0.8", "apollo-link": "^1.2.3", "apollo-link-ws": "^1.0.9", "apollo-utilities": "^1.0.21", "awesome-typescript-loader": "^5.2.1", "babel-loader": "^8.0.2", "babel-plugin-styled-components": "^1.10.6", "body-parser": "^1.18.3", "chokidar": "^2.0.4", "chrome-launcher": "^0.10.5", "consola": "^2.5.7", "copy-webpack-plugin": "^5.0.1", "cross-env": "^5.2.0", "css-loader": "^1.0.0", "file-loader": "^3.0.1", "get-port": "^4.2.0", "graphql-tag": "^2.9.2", "graphql-yoga": "^1.16.1", "html-webpack-include-assets-plugin": "^1.0.5", "html-webpack-plugin": "^3.2.0", "html-webpack-template": "^6.2.0", "istanbul-lib-coverage": "^2.0.3", "istanbul-lib-source-maps": "^3.0.2", "launch-editor": "^2.2.1", "lodash.throttle": "^4.1.1", "minimist": "^1.2.0", "nanoid": "^2.0.0", "nodemon": "^1.18.3", "np": "^4.0.2", "react": "^16.8.3", "react-apollo": "^2.1.11", "react-apollo-hooks": "^0.2.1", "react-dom": "^16.8.3", "react-feather": "^1.1.4", "react-inspector": "^3.0.0", "react-split-pane": "^0.1.84", "react-spring": "^8.0.9", "react-tippy": "^1.2.3", "react-virtualized-auto-sizer": "^1.0.2", "react-window": "^1.6.2", "reflect-metadata": "^0.1.12", "resolve-pkg": "^1.0.0", "rimraf": "^2.6.2", "style-loader": "^0.23.0", "styled-components": "^4.1.3", "styled-system": "^3.1.11", "svg-inline-loader": "^0.8.0", "svg-react-loader": "^0.4.5", "ts-node": "^7.0.1", "type-graphql": "^0.14.0", "typeface-open-sans": "^0.0.54", "typescript": "^3.0.1", "uglifyjs-webpack-plugin": "^2.0.0", "url-loader": "^1.1.1", "webpack": "^4.17.1", "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.2.1" }, "resolutions": { "graphql": "^0.13.0" }, "bin": { "majestic": "./dist/server/index.js" }, "files": [ "/dist/**", "/yarn.lock" ] } ================================================ FILE: scripts/webpack.server.config.js ================================================ const CopyPlugin = require('copy-webpack-plugin'); const webpack = require('webpack'); const path = require('path'); module.exports = env => ({ entry: './server/index.ts', mode: 'production', target: 'node', output: { path: path.resolve(__dirname, '../dist/server'), filename: 'index.js', libraryTarget: 'commonjs2', }, resolve: { mainFields: ['main'], extensions: ['.ts', '.js', '.jsx'], }, optimization: { minimize: false, }, devtool: 'source-map', module: { rules: [ { test: /\.js$/, exclude: /(node_modules)/, loader: 'babel-loader', }, { test: /\.ts$/, exclude: /(node_modules)/, loader: 'awesome-typescript-loader', options: { transpileOnly: true, configFileName: './tsconfig.server.json', }, }, ], }, plugins: [ new webpack.DefinePlugin({ PRODUCTION: env.production === 'production', }), new CopyPlugin([ { from: './server/services/jest-manager/scripts', to: './scripts' }, ]), new webpack.BannerPlugin({ banner: '#!/usr/bin/env node', raw: true, }), ], externals: ['read-pkg-up', 'open'], node: { __dirname: false, }, }); ================================================ FILE: scripts/webpack.ui.config.js ================================================ const HtmlWebpackPlugin = require('html-webpack-plugin'); const webpack = require('webpack'); const path = require('path'); module.exports = env => ({ entry: './ui/index.tsx', mode: env.production ? 'production' : 'development', output: { path: path.resolve(__dirname, '../dist/ui'), filename: 'ui.bundle.js', }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, devServer: { contentBase: path.resolve(__dirname, '../dist/ui'), hot: true, port: 9000, }, devtool: 'source-map', module: { rules: [ { test: /\.(js|jsx|ts|tsx)$/, exclude: /(node_modules)/, loader: 'babel-loader', }, { test: /\.css$/, use: ['style-loader', 'css-loader'], }, { test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, use: { loader: 'url-loader', options: { limit: 50000, }, }, }, { test: /\.(graphql|gql)$/, exclude: /node_modules/, loader: 'graphql-tag/loader', }, { test: /\.(png|svg|jpg|gif)$/, use: [ { loader: 'file-loader', options: { name: '[name].[ext]', }, }, ], }, ], }, plugins: [ new HtmlWebpackPlugin({ title: 'Majestic', template: require('html-webpack-template'), appMountId: 'root', inject: false, favicon: './ui/assets/favicon.ico', }), new webpack.HotModuleReplacementPlugin(), new webpack.DefinePlugin({ PRODUCTION: env.production === true, }), ], }); ================================================ FILE: server/api/app/app.ts ================================================ import { ObjectType, Field } from "type-graphql"; @ObjectType() export class App { @Field({ nullable: true }) selectedFile: string; } ================================================ FILE: server/api/app/resolver.ts ================================================ import { Resolver, Mutation, Arg, Query } from "type-graphql"; import * as launch from "launch-editor"; import { App } from "./app"; import FileWatcher, { WatcherEvents } from "../../services/file-watcher"; import { pubsub } from "../../event-emitter"; import { dirname, basename } from "path"; @Resolver(App) export default class AppResolver { private appInstance: App; private fileWatcher: FileWatcher; constructor() { this.fileWatcher = new FileWatcher(); this.appInstance = new App(); } @Query(returns => App) app() { return this.appInstance; } @Mutation(returns => App) setSelectedFile(@Arg("path", { nullable: true }) path: string) { this.appInstance.selectedFile = path; if (path) { this.fileWatcher.watch(path); pubsub.publish(WatcherEvents.FILE_CHANGE, { id: WatcherEvents.FILE_CHANGE, payload: { path } }); } return this.appInstance; } @Mutation(returns => String) openInEditor(@Arg("path") path: string) { launch(path, process.env.EDITOR || "code", (path: string, err: any) => { console.log("Failed to open file in editor. You may need to install the code command to your PATH if you are using VSCode: ", err); }); return ""; } @Mutation(returns => String) openSnapInEditor(@Arg("path") path: string) { var dir = dirname(path) var file = basename(path); var snap = dir + '/__snapshots__/' + file + '.snap' console.log("opening the snapshot:", snap); this.openInEditor(snap); return ""; } @Mutation(returns => String) openFailure(@Arg("failure") failure: string) { // The following regex matches the first line of the form: \w at () // it captures and returns that in the second position of the match array let re = new RegExp('^\\s+at.*?\\((.*?)\\)$', 'm'); let match = failure.match(re); if (match && match.length === 2) { const path = match[1]; launch(path, process.env.EDITOR || "code", (path: string, err: any) => { console.log("Failed to open file in editor. You may need to install the code command to your PATH if you are using VSCode: ", err); }); } else { console.log("Failed to find a path to a file to load in the failure string."); } return ""; } } ================================================ FILE: server/api/index.ts ================================================ import { buildSchema } from "type-graphql"; import { pubsub } from "../event-emitter"; import Workspace from "./workspace/resolver"; import Runner from "./runner/resolver"; import App from "./app/resolver"; export async function getSchema() { return await buildSchema({ resolvers: [Workspace, Runner, App], pubSub: pubsub as any }); } ================================================ FILE: server/api/runner/resolver.ts ================================================ import { Resolver, Mutation, Arg, Query, Subscription, Root } from "type-graphql"; import { Runner } from "./type"; import JestManager, { RunnerEvents, RunnerEvent } from "../../services/jest-manager"; import Workspace from "../../services/project"; import { root } from "../../services/cli"; import { RunnerStatus } from "./status"; import { pubsub } from "../../event-emitter"; import ConfigResolver from "../../services/config-resolver"; @Resolver(Runner) export default class RunnerResolver { private jestManager: JestManager; private workspace: Workspace; private isRunning: boolean; private activeFile: string; private isWatching: boolean = false; private collectCoverage: boolean = false; constructor() { this.workspace = new Workspace(root); const configResolver = new ConfigResolver(); const majesticConfig = configResolver.getConfig(root); this.jestManager = new JestManager(this.workspace, majesticConfig); } @Query(returns => RunnerStatus) runnerStatus() { const status = new RunnerStatus(); status.activeFile = this.activeFile; status.running = this.isRunning; status.watching = this.isWatching; return status; } @Query(returns => Boolean) shouldCollectCoverage() { return this.collectCoverage; } @Subscription(returns => RunnerStatus, { topics: [ RunnerEvents.RUNNER_STARTED, RunnerEvents.RUNNER_STOPPED, RunnerEvents.RUNNER_WATCH_MODE_CHANGE, RunnerEvents.RUNNER_ACTIVE_FILE_CHANGE ] }) runnerStatusChange(@Root() event: RunnerEvent) { this.isRunning = event.payload.isRunning !== undefined ? event.payload.isRunning : this.isRunning; const status = new RunnerStatus(); status.activeFile = this.activeFile; status.running = this.isRunning; status.watching = this.isWatching; return status; } @Mutation(returns => String, { nullable: true }) runFile(@Arg("path") path: string) { this.activeFile = path; if (this.isWatching && this.isRunning) { pubsub.publish(RunnerEvents.RUNNER_ACTIVE_FILE_CHANGE, { id: RunnerEvents.RUNNER_ACTIVE_FILE_CHANGE, payload: {} }); return this.jestManager.switchToAnotherFile(path); } return this.jestManager.runSingleFile( path, this.isWatching, this.collectCoverage ); } @Mutation(returns => String, { nullable: true }) run() { this.activeFile = ""; this.isRunning = true; return this.jestManager.run(this.isWatching, this.collectCoverage); } @Mutation(returns => String, { nullable: true }) stop() { return this.jestManager.stop(); } @Mutation(returns => String, { nullable: true }) updateSnapshot(@Arg("path") path: string) { this.activeFile = path; return this.jestManager.updateSnapshotToFile(path); } @Mutation(returns => RunnerStatus, { nullable: true }) toggleWatch(@Arg("watch") watch: boolean) { this.isWatching = watch; pubsub.publish(RunnerEvents.RUNNER_WATCH_MODE_CHANGE, { id: RunnerEvents.RUNNER_WATCH_MODE_CHANGE, payload: {} }); } @Mutation(returns => Boolean) setCollectCoverage(@Arg("collect") collect: boolean) { this.collectCoverage = collect; return this.collectCoverage; } } ================================================ FILE: server/api/runner/status.ts ================================================ import { ObjectType, Field, ID } from "type-graphql"; @ObjectType() export class RunnerStatus { @Field({ nullable: true }) running: boolean; @Field({ nullable: true }) activeFile: string; @Field({ nullable: true }) watching: boolean; } ================================================ FILE: server/api/runner/type.ts ================================================ import { ObjectType, Field, ID } from "type-graphql"; @ObjectType() export class Runner { @Field() status: string; @Field() config: string; } ================================================ FILE: server/api/workspace/coverage.ts ================================================ import { ObjectType, Field } from "type-graphql"; @ObjectType() export class CoverageSummary { @Field({ nullable: true }) statement: number = 0; @Field({ nullable: true }) function: number = 0; @Field({ nullable: true }) branch: number = 0; @Field({ nullable: true }) line: number = 0; } ================================================ FILE: server/api/workspace/resolver.ts ================================================ import { Resolver, Arg, Query, Subscription, Root, Mutation } from "type-graphql"; import * as throttle from "lodash.throttle"; import { Workspace } from "./workspace"; import Project from "../../services/project"; import { root } from "../../services/cli"; import { RunnerEvents } from "../../services/jest-manager"; import { TestFile } from "./test-file"; import { inspect } from "../../services/ast/inspector"; import { TestFileResult } from "./test-result/file-result"; import { Events, ResultEvent, SummaryEvent } from "../../services/result-handler-api"; import Results from "../../services/results"; import { WatcherEvents, FileChangeEvent } from "../../services/file-watcher"; import { Summary } from "./summary"; import { pubsub } from "../../event-emitter"; import ConfigResolver from "../../services/config-resolver"; import { MajesticConfig } from "../../services/types"; const SummaryEvent: "SummaryEvent" = "SummaryEvent"; @Resolver(Workspace) export default class WorkspaceResolver { private project: Project; private results: Results; private majesticConfig: MajesticConfig; constructor() { this.project = new Project(root); const configResolver = new ConfigResolver(); this.majesticConfig = configResolver.getConfig(root); this.results = new Results(root); this.results.getCoverageReportPath(this.majesticConfig); pubsub.publish("WorkspaceInitialized", { coverageDirectory: this.results.coverageDirectory }); this.results.checkIfCoverageReportExists(); pubsub.subscribe(Events.TEST_RESULT, ({ payload }: any) => { const result = new TestFileResult(); result.path = payload.path; result.failureMessage = payload.failureMessage; result.numPassingTests = payload.numPassingTests; result.numFailingTests = payload.numFailingTests; result.numPendingTests = payload.numPendingTests; result.testResults = payload.testResults; result.consoleLogs = payload.console; this.results.setTestReport(payload.path, result); this.notifySummaryChange(); }); pubsub.subscribe(Events.TEST_START, ({ payload }: any) => { this.results.setTestStart(payload.path); this.notifySummaryChange(); }); pubsub.subscribe(Events.RUN_SUMMARY, ({ payload }: any) => { const { numFailedTests, numPassedTests, numPassedTestSuites, numFailedTestSuites } = payload.summary; this.results.setSummary( numPassedTests, numFailedTests, numPassedTestSuites, numFailedTestSuites ); this.notifySummaryChange(); }); pubsub.subscribe(Events.RUN_COMPLETE, ({ payload }) => { this.results.mapCoverage(payload.coverageMap); setTimeout(() => { this.results.checkIfCoverageReportExists(); this.notifySummaryChange(); }, 2000); }); pubsub.subscribe(RunnerEvents.RUNNER_STOPPED, () => { this.results.markExecutingAsStopped(); }); } private notifySummaryChange = throttle(() => { pubsub.publish(SummaryEvent, {}); }, 1000); @Query(returns => Workspace) workspace() { const workspace = new Workspace(); workspace.projectRoot = this.project.projectRoot; workspace.name = "Jest project"; const fileMap = this.project.getFilesList(this.majesticConfig); workspace.files = Object.entries(fileMap).map(([key, value]: any) => ({ name: value.name, path: value.path, parent: value.parent, type: value.type })); return workspace; } @Query(returns => TestFile) async file(@Arg("path") path: string) { const file = new TestFile(); file.items = await inspect(path); return file; } @Query(returns => TestFileResult, { nullable: true }) result(@Arg("path") path: string) { const result = this.results.getResult(path); return result ? result : null; } @Subscription(returns => TestFile, { topics: [WatcherEvents.FILE_CHANGE] }) async fileChange(@Root() event: FileChangeEvent, @Arg("path") path: string) { const file = new TestFile(); file.items = await inspect(event.payload.path); return file; } @Subscription(returns => TestFileResult, { topics: [ Events.TEST_START, Events.TEST_RESULT, RunnerEvents.RUNNER_STOPPED ], filter: ({ payload: { payload }, args }) => { return payload.path === args.path; } }) async changeToResult( @Root() event: ResultEvent, @Arg("path") path: string ): Promise { const payload = event.payload; const result = new TestFileResult(); if (event.id === Events.TEST_START) { const existingResults = this.results.getResult(path); if (existingResults) { result.testResults = existingResults.testResults; } } else if (event.id === Events.TEST_RESULT) { result.path = path; result.failureMessage = payload.failureMessage; result.numPassingTests = payload.numPassingTests; result.numFailingTests = payload.numFailingTests; result.numPendingTests = payload.numPendingTests; result.testResults = payload.testResults; result.consoleLogs = payload.console; } return result; } @Subscription(returns => Summary, { topics: [SummaryEvent] }) async changeToSummary(@Root() event: SummaryEvent): Promise { const { numFailedTests, numPassedTests, numPassedTestSuites, numFailedTestSuites } = this.results.getSummary(); const summary = new Summary(); summary.numFailedTests = numFailedTests; summary.numPassedTests = numPassedTests; summary.numPassedTestSuites = numPassedTestSuites; summary.numFailedTestSuites = numFailedTestSuites; summary.failedTests = this.results.getFailedTests(); summary.executingTests = this.results.getExecutingTests(); summary.passingTests = this.results.getPassedTests(); summary.coverage = this.results.getCoverage(); summary.haveCoverageReport = this.results.doesHaveCoverageReport(); return summary; } @Query(returns => Summary, { nullable: true }) summary() { const { numFailedTests, numPassedTests, numPassedTestSuites, numFailedTestSuites } = this.results.getSummary(); const result = new Summary(); result.numFailedTests = numFailedTests; result.numPassedTests = numPassedTests; result.numPassedTestSuites = numPassedTestSuites; result.numFailedTestSuites = numFailedTestSuites; result.failedTests = this.results.getFailedTests(); result.executingTests = this.results.getExecutingTests(); result.passingTests = this.results.getPassedTests(); result.coverage = this.results.getCoverage(); result.haveCoverageReport = this.results.doesHaveCoverageReport(); return result; } } ================================================ FILE: server/api/workspace/summary.ts ================================================ import { ObjectType, Field } from "type-graphql"; import { CoverageSummary } from "./coverage"; @ObjectType() export class Summary { @Field({ nullable: true }) numPassedTests: number = 0; @Field({ nullable: true }) numFailedTests: number = 0; @Field({ nullable: true }) numPassedTestSuites: number = 0; @Field({ nullable: true }) numFailedTestSuites: number = 0; @Field(returns => [String]) passingTests: string[] = []; @Field(returns => [String]) failedTests: string[] = []; @Field(returns => [String]) executingTests: string[] = []; @Field(returns => CoverageSummary, { nullable: true }) coverage: CoverageSummary; @Field(returns => Boolean, { nullable: true }) haveCoverageReport: boolean; } ================================================ FILE: server/api/workspace/test-file.ts ================================================ import { ObjectType, Field } from "type-graphql"; import { TestItem } from "./test-item"; @ObjectType() export class TestFile { @Field(returns => [TestItem]) items: TestItem[]; } ================================================ FILE: server/api/workspace/test-item.ts ================================================ import { ObjectType, Field } from "type-graphql"; export type TestItemType = "describe" | "it" | "todo"; @ObjectType() export class TestItem { @Field() id: string; @Field({ nullable: true }) name: string; @Field() type: TestItemType; @Field({ nullable: true }) parent?: string; @Field() only: boolean; } ================================================ FILE: server/api/workspace/test-result/console-log.ts ================================================ import { ObjectType, Field } from "type-graphql"; @ObjectType() export class ConsoleLog { @Field({ nullable: true }) message: string; @Field({ nullable: true }) origin: string; @Field({ nullable: true }) type: string; } ================================================ FILE: server/api/workspace/test-result/file-result.ts ================================================ import { ObjectType, Field } from "type-graphql"; import { TestItemResult } from "./test-item-result"; import { ConsoleLog } from "./console-log"; @ObjectType() export class TestFileResult { @Field({ nullable: true }) path: string; @Field({ nullable: true }) numFailingTests: number = 0; @Field({ nullable: true }) numPassingTests: number = 0; @Field({ nullable: true }) numPendingTests: number = 0; @Field({ nullable: true }) failureMessage: string; @Field(returns => TestItemResult, { nullable: true }) testResults: TestItemResult[] | null; @Field(returns => ConsoleLog, { nullable: true }) consoleLogs: ConsoleLog[]; } ================================================ FILE: server/api/workspace/test-result/test-item-result.ts ================================================ import { ObjectType, Field } from "type-graphql"; @ObjectType() export class TestItemResult { @Field() title: string; @Field() numPassingAsserts: number; @Field() status: string; @Field(returns => [String]) failureMessages: string[] = []; @Field(returns => [String]) ancestorTitles: string[] = []; @Field() duration: number; } ================================================ FILE: server/api/workspace/tree.ts ================================================ import { ObjectType, Field, ID } from "type-graphql"; @ObjectType() export class Item { @Field() path: string; @Field() name: string; @Field() type: "directory" | "file"; @Field({ nullable: true }) parent?: string; } ================================================ FILE: server/api/workspace/workspace.ts ================================================ import { ObjectType, Field, ID } from "type-graphql"; import { Item } from "./tree"; @ObjectType() export class Workspace { @Field() projectRoot: string; @Field() name: string; @Field(type => [Item]) files: Item[]; } ================================================ FILE: server/event-emitter/index.ts ================================================ import { PubSub } from "graphql-yoga"; export const pubsub = new PubSub(); ================================================ FILE: server/index.ts ================================================ import { GraphQLServer } from "graphql-yoga"; import "reflect-metadata"; import { getSchema } from "./api"; import resultHandlerApi from "./services/result-handler-api"; import getPort from "get-port"; import * as parseArgs from "minimist"; import * as chromeLauncher from "chrome-launcher"; import * as opn from "open"; import "consola"; import { initializeStaticRoutes } from "./static-files"; import { root } from "./services/cli"; import * as readPkgUp from "read-pkg-up"; const pkg = readPkgUp.sync({ cwd: __dirname }).pkg; declare var consola: any; const args = parseArgs(process.argv); const defaultPort = args.port || 4000; process.env.DEBUG_LOG = args.debug ? "log" : ""; if (args.root) { process.env.ROOT = args.root; } if (args.version) { console.log(`v${pkg.version}`); process.exit(); } async function main() { try { const schema: any = await getSchema(); const server = new GraphQLServer({ schema }); initializeStaticRoutes(server.express, root); resultHandlerApi(server.express); const port = await getPort({ port: defaultPort }); // this will be used by the jest reporter process.env.MAJESTIC_PORT = port.toString(); server.start( { port, playground: "/debug" }, async () => { const url = `http://localhost:${port}`; console.log(`⚡ Majestic v${pkg.version} is running at ${url} `); if (args.app) { await chromeLauncher.launch({ startingUrl: url, chromeFlags: [`--app=${url}`] }); } else if (!args.noOpen) { opn(url); } } ); } catch (e) { consola.error(e); } } main(); ================================================ FILE: server/logger.ts ================================================ declare var consola: any; export function debugLog(tag: string, ...args: any) { if (process.env.DEBUG_LOG !== "") { consola.info({ tag, args }); } } export function executeAndLog( tag: string, message: string, execute: () => any ) { if (process.env.DEBUG_LOG !== "") { consola.info({ tag, args: [message, execute()] }); } } export function createLogger(tag: string) { return (...args: any) => debugLog(tag, ...args); } ================================================ FILE: server/services/ast/inspector.ts ================================================ import traverse from "@babel/traverse"; import * as nanoid from "nanoid"; import { parse } from "./parser"; import { readFile } from "fs"; import { TestItem, TestItemType } from "../../api/workspace/test-item"; export async function inspect(path: string): Promise { return new Promise((resolve, reject) => { readFile( path, { encoding: "utf8" }, (err, code) => { if (err) { reject(err); } let ast; try { ast = parse(path, code); } catch (e) { reject(e); } const result: TestItem[] = []; traverse(ast, { CallExpression(path: any) { if (path.scope.block.type === "Program") { findItems(path, result); } } }); resolve(result); } ); }); } function getTemplateLiteralName(path: any) { let currentExpressionIndex = 0; const { expressions, quasis } = path.node.arguments[0]; return `\`${quasis.reduce((finalText: String, q: any) => { if ( expressions[currentExpressionIndex] && q.end === expressions[currentExpressionIndex].start - 2 ) { const formattedExpression = `${q.value.raw}\$\{${expressions[currentExpressionIndex].name}\}`; currentExpressionIndex += 1; return finalText.concat(formattedExpression); } else { return finalText.concat(q.value.raw); } }, '')}\``; } function findItems(path: any, result: TestItem[], parentId?: any) { let type: string; let only: boolean = false; if (path.node.callee.name === "fdescribe") { type = "describe"; only = true; } else if (path.node.callee.name === "fit") { type = "it"; only = true; } else if ( path.node.callee.property && path.node.callee.property.name === "only" ) { type = path.node.callee.object.name; only = true; } else if (path.node.callee.name === "test") { type = "it"; } else if ( path.node.callee.property && path.node.callee.property.name === "todo" ) { type = "todo"; } else { type = path.node.callee.name; } if (type === "describe") { let describe: any; if (path.node.arguments[0].type === "TemplateLiteral") { describe = { id: nanoid(), type: "describe" as TestItemType, name: getTemplateLiteralName(path), only, parent: parentId }; } else { describe = { id: nanoid(), type: "describe" as TestItemType, name: path.node.arguments[0].value, only, parent: parentId }; } result.push(describe); path.skip(); path.traverse({ CallExpression(itPath: any) { findItems(itPath, result, describe.id); } }); } else if (type === "it") { if (path.node.arguments[0].type === "TemplateLiteral") { result.push({ id: nanoid(), type: "it", name: getTemplateLiteralName(path), only, parent: parentId }); } else { result.push({ id: nanoid(), type: "it", name: path.node.arguments[0].value, only, parent: parentId }); } } else if (type === "todo") { if (path.node.arguments[0].type === "TemplateLiteral") { result.push({ id: nanoid(), type: "todo", name: getTemplateLiteralName(path), only, parent: parentId }); } else { result.push({ id: nanoid(), type: "todo", name: path.node.arguments[0].value, only, parent: parentId }); } } } ================================================ FILE: server/services/ast/parser.ts ================================================ import * as parser from "@babel/parser"; import { extname } from "path"; export function parse(path: string, code: string) { const isTS = [".ts", ".tsx"].indexOf(extname(path).toLowerCase()) > -1; const additionalPlugin = isTS ? "typescript" : "flow"; return parser.parse(code, { sourceType: "module", plugins: ["jsx", "classProperties", "optionalChaining", additionalPlugin] }); } ================================================ FILE: server/services/cli.ts ================================================ export const root = process.env.ROOT || process.cwd(); ================================================ FILE: server/services/config-resolver.ts ================================================ import * as parseArgs from "minimist"; import * as readPkgUp from "read-pkg-up"; import * as resolvePkg from "resolve-pkg"; import { MajesticConfig } from "./types"; import { platform } from "os"; import { join } from "path"; import { existsSync } from "fs"; import { createLogger } from "../logger"; declare var consola: any; const log = createLogger("Config Resolver"); export default class ConfigResolver { public getConfig(projectRoot: string): MajesticConfig { let jestScriptPath = null; let args: string[] = []; let env: any = {}; const configFromPkgJson = this.getConfigFromPackageJson(projectRoot) || {}; const jestScriptPathFromPackage = configFromPkgJson.jestScriptPath ? join(projectRoot, configFromPkgJson.jestScriptPath) : null; if (this.isBootstrappedWithCreateReactApp(projectRoot)) { log("Project identified as Create react app"); jestScriptPath = jestScriptPathFromPackage || this.getJestScriptForCreateReactApp(projectRoot); args = ["--env=jsdom"]; env = { CI: "true" }; } else { log("Majestic configuration from Package.json: ", configFromPkgJson); jestScriptPath = jestScriptPathFromPackage || this.getJestScriptPath(projectRoot); } const configArg = parseArgs(process.argv).config; if (configArg && configFromPkgJson.configs) { args = [...args, ...(configFromPkgJson.configs[configArg].args || [])]; env = { ...env, ...(configFromPkgJson.configs[configArg].env || {}) }; } else { args = [...args, ...(configFromPkgJson.args || [])]; env = { ...env, ...(configFromPkgJson.env || {}) }; } const majesticConfig = { jestScriptPath: `"${jestScriptPath}"`, args, env }; log("Resolved Majestic config :", majesticConfig); return majesticConfig; } private getJestScriptPath(projectRoot: string) { const path = resolvePkg("jest", { cwd: projectRoot }); log("Path of resolved Jest script: ", path); if (!path) { consola.error( "🚨 Majestic was unable to find Jest package in node_modules folder. But you can provide the path manually. Please take a look at the documentation at https://github.com/Raathigesh/majestic." ); process.exit(); } return join(path, "bin/jest.js"); } private getJestScriptForCreateReactApp(projectRoot: string) { const path = resolvePkg("react-scripts", { cwd: projectRoot }); return join(path, "scripts/test.js"); } private getPackageJson(rootPath: string) { return readPkgUp.sync({ cwd: rootPath }).pkg; } private getConfigFromPackageJson(projectRoot: string) { const packageJson = this.getPackageJson(projectRoot); if (packageJson.majestic) { return packageJson.majestic; } return null; } private isBootstrappedWithCreateReactApp(rootPath: string): boolean { return ( this.hasExecutable(rootPath, "node_modules/.bin/react-scripts") || this.hasExecutable( rootPath, "node_modules/react-scripts/node_modules/.bin/jest" ) || this.hasExecutable(rootPath, "node_modules/react-native-scripts") ); } private hasExecutable(rootPath: string, executablePath: string): boolean { const ext = platform() === "win32" ? ".cmd" : ""; const absolutePath = join(rootPath, executablePath + ext); return existsSync(absolutePath); } } ================================================ FILE: server/services/file-watcher/index.ts ================================================ import { pubsub } from "../../event-emitter"; import { watch } from "fs"; import { createLogger } from "../../logger"; const log = createLogger("File watcher"); export const WatcherEvents = { FILE_CHANGE: "FILE_CHANGE" }; export interface FileChangeEvent { id: string; payload: { path: string; }; } export default class FileWatcher { private watcher: any; watch(filePath: string) { if (this.watcher) { this.watcher.close(); log("Closed existing file watcher"); } log("Watching file :", filePath); this.watcher = watch(filePath, () => { log("File changed", filePath); pubsub.publish(WatcherEvents.FILE_CHANGE, { id: WatcherEvents.FILE_CHANGE, payload: { path: filePath } }); }); } } ================================================ FILE: server/services/jest-manager/cli-args.ts ================================================ export const ShowConfig = "--showConfig"; ================================================ FILE: server/services/jest-manager/index.ts ================================================ import { spawn, ChildProcess, execSync } from "child_process"; import { join } from "path"; import Project from "../project"; import { pubsub } from "../../event-emitter"; import { MajesticConfig } from "../types"; import { createLogger } from "../../logger"; const log = createLogger("Jest Manager"); export const RunnerEvents = { RUNNER_STARTED: "RunnerStarted", RUNNER_STOPPED: "RunnerStopped", RUNNER_WATCH_MODE_CHANGE: "WatchModeChanged", RUNNER_ACTIVE_FILE_CHANGE: "RunnerActiveFileChange" }; export interface RunnerEvent { id: string; payload: { isRunning: boolean; }; } export default class JestManager { project: Project; process: ChildProcess; config: MajesticConfig; constructor(project: Project, config: MajesticConfig) { this.project = project; this.config = config; } run(watch: boolean, collectCoverage: boolean) { this.executeJest( [ "--reporters", this.getReporterPath(), ...(watch ? [this.getWatchFlag()] : []) ], true, true, collectCoverage ); } runSingleFile(path: string, watch: boolean, collectCoverage: boolean) { this.executeJest( [ this.getPatternForPath(path), ...(watch ? [this.getWatchFlag()] : []), "--reporters", "default", this.getReporterPath(), "--verbose=false" // this would allow jest to include console output in the result of reporter ], !watch, // while watching, can not inherit stdio because we want to write back and interact with the process false, collectCoverage ); } updateSnapshotToFile(path: string) { this.executeJest( [ this.getPatternForPath(path), "-u", "--reporters", this.getReporterPath() ], false, false, false ); } switchToAnotherFile(path: string) { this.executeInSequence([ { fn: () => this.process.stdin && this.process.stdin.write("p"), delay: 0 }, { fn: () => this.process.stdin && this.process.stdin.write(this.getPatternForPath(path)), delay: 100 }, { fn: () => this.process.stdin && this.process.stdin.write(new Buffer("0d", "hex").toString()), delay: 200 } ]); } executeJest( args: string[] = [], inherit: boolean, shouldReportSummary: boolean, collectCoverage: boolean ) { if (!this.config.jestScriptPath) { throw new Error("Jest script path is empty"); } this.reportStart(); const finalArgs = [ "-r", this.getPatchFilePath(), this.config.jestScriptPath, ...(this.config.args || []), "--colors", ...(collectCoverage ? ["--collectCoverage=true"] : ["--collectCoverage=false"]), ...args ]; const finalEnv = { ...(this.config.env || {}), MAJESTIC_PORT: process.env.MAJESTIC_PORT, REPORT_SUMMARY: shouldReportSummary ? "report" : "" }; log("Executing Jest with :", finalArgs, finalEnv); this.process = spawn("node", finalArgs, { cwd: this.project.projectRoot, shell: true, stdio: inherit ? "inherit" : "pipe", env: { ...(process.env || {}), ...finalEnv } }); this.process.on("exit", () => { this.reportStop(); }); this.process.stdout && this.process.stdout.on("data", (data: string) => { console.log(data.toString().trim()); }); this.process.stderr && this.process.stderr.on("data", (data: string) => { console.log(data.toString().trim()); }); } getReporterPath() { return `"${join(__dirname, "./scripts/reporter.js")}"`; } getPatchFilePath() { return `"${join(__dirname, "./scripts/patch.js")}"`; } getPatternForPath(path: string) { let replacePattern = /\//g; if (process.platform === "win32") { replacePattern = /\\/g; } return `^${path.replace(replacePattern, ".")}$`; } reportStart() { pubsub.publish(RunnerEvents.RUNNER_STARTED, { id: RunnerEvents.RUNNER_STARTED, payload: { isRunning: true } }); } stop() { if (this.process) { if (process.platform === "win32") { // Windows doesn't exit the process when it should. spawn("taskkill", ["/pid", "" + this.process.pid, "/T", "/F"]); } else { this.process.kill(); } this.reportStop(); } } reportStop() { pubsub.publish(RunnerEvents.RUNNER_STOPPED, { id: RunnerEvents.RUNNER_STOPPED, payload: { isRunning: false } }); } async executeInSequence( funcs: Array<{ fn: () => void; delay: number; }> ) { for (const { fn, delay } of funcs) { await this.setTimeoutPromisify(fn, delay); } } setTimeoutPromisify(fn: () => void, delay: number) { return new Promise(resolve => { setTimeout(() => { fn(); resolve(); }, delay); }); } getWatchFlag() { return this.isInGitRepository() ? "--watch" : "--watchAll"; } isInGitRepository() { try { execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" }); return true; } catch (e) { return false; } } } ================================================ FILE: server/services/jest-manager/scripts/patch.js ================================================ // Monkey patch the stdin with setRawMode so jest would think it's running from a terminal process.stdin.setRawMode = () => {}; ================================================ FILE: server/services/jest-manager/scripts/reporter.js ================================================ const fetch = require('node-fetch'); function send(type, body) { fetch('http://localhost:' + process.env.MAJESTIC_PORT + '/' + type, { method: 'post', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, }); } class MyCustomReporter { constructor(globalConfig, options) { this._globalConfig = globalConfig; this._options = options; } onTestStart(test) { send('test-start', { path: test.path, }); } onTestResult(test, testResult, aggregatedResult) { send('test-result', { path: testResult.testFilePath, failureMessage: testResult.failureMessage, numFailingTests: testResult.numFailingTests, numPassingTests: testResult.numPassingTests, numPendingTests: testResult.numPendingTests, testResults: (testResult.testResults || []).map(result => ({ title: result.title, numPassingAsserts: result.numPassingAsserts, status: result.status, failureMessages: result.failureMessages, ancestorTitles: result.ancestorTitles, duration: result.duration, })), aggregatedResult: process.env.REPORT_SUMMARY === 'report' ? { numFailedTests: aggregatedResult.numFailedTests, numPassedTests: aggregatedResult.numPassedTests, numPassedTestSuites: aggregatedResult.numPassedTestSuites, numFailedTestSuites: aggregatedResult.numFailedTestSuites, } : null, console: testResult.console, }); } onRunStart(results) {} onRunComplete(contexts, results) { send('run-complete', { coverageMap: results.coverageMap, }); } } module.exports = MyCustomReporter; ================================================ FILE: server/services/project.ts ================================================ import { TreeMap, MajesticConfig } from "./types"; import { spawnSync } from "child_process"; import { sep, join, extname, normalize } from "path"; import { createLogger } from "../logger"; const log = createLogger("Project"); export default class Project { public projectRoot: string; constructor(root: string) { this.projectRoot = normalize(root); } getFilesList(config: MajesticConfig) { const configProcess = spawnSync( "node", [config.jestScriptPath, ...(config.args || []), "--listTests", "--json"], { cwd: this.projectRoot, shell: true, stdio: "pipe", env: { CI: "true", ...(config.env || {}), ...process.env } } ); const filesStr = configProcess.stdout.toString().trim(); const files: string[] = JSON.parse(filesStr); log("Identified test files: ", files); const relativeFiles = files.map(file => file.replace(this.projectRoot, "")); const map: TreeMap = { "/": { name: this.projectRoot.split(sep).pop() || "", type: "directory", path: this.projectRoot, parent: undefined } }; relativeFiles.forEach(path => { const tokens = path.split(sep).filter(token => token.trim() !== ""); let currentPath = ""; let parentPath = ""; tokens.forEach((token, i) => { currentPath = `${currentPath}${sep}${token}`; const type = [".jsx", ".tsx", ".ts", ".js"].includes( extname(currentPath) ) ? "file" : "directory"; if (!map[currentPath]) { map[currentPath] = { name: token, type, path: join(this.projectRoot, currentPath), parent: join(this.projectRoot, parentPath) }; } parentPath = currentPath; }); }); return map; } } ================================================ FILE: server/services/result-handler-api.ts ================================================ import { Application } from "express"; import * as bodyParser from "body-parser"; import { pubsub } from "../event-emitter"; import { createLogger } from "../logger"; const log = createLogger("Report API"); export const Events = { TEST_START: "TEST_START", TEST_RESULT: "TEST_RESULT", RUN_START: "RUN_START", RUN_COMPLETE: "RUN_COMPLETE", RUN_SUMMARY: "RUN_SUMMARY" }; export interface ResultEvent { id: string; payload: any; } export interface SummaryEvent { id: string; payload: { summary: { numPassedTests: number; numFailedTests: number; numPassedTestSuites: number; numFailedTestSuites: number; }; }; } export default function handlerApi(expressApp: Application) { expressApp.use( bodyParser.json({ limit: "50mb" }) ); expressApp.post("/test-start", ({ body }, res) => { log("File execution start reported ", body.path); pubsub.publish(Events.TEST_START, { id: Events.TEST_START, payload: { path: body.path } }); res.send("ok"); }); expressApp.post("/test-result", ({ body }, res) => { log("File result reported ", body.path); pubsub.publish(Events.TEST_RESULT, { id: Events.TEST_RESULT, payload: body }); if (body.aggregatedResult) { pubsub.publish(Events.RUN_SUMMARY, { id: Events.RUN_SUMMARY, payload: { summary: body.aggregatedResult } }); } res.send("ok"); }); expressApp.post("/run-start", (req, res) => { pubsub.publish(Events.RUN_START, { id: Events.RUN_START, payload: req.body }); res.send("ok"); }); expressApp.post("/run-complete", (req, res) => { pubsub.publish(Events.RUN_COMPLETE, { id: Events.RUN_COMPLETE, payload: req.body }); res.send("ok"); }); } ================================================ FILE: server/services/results.ts ================================================ import { createSourceMapStore, MapStore } from "istanbul-lib-source-maps"; import { createCoverageMap, CoverageMap } from "istanbul-lib-coverage"; import { existsSync } from "fs"; import { join } from "path"; import { MajesticConfig } from "./types"; import { spawnSync } from "child_process"; import { createLogger } from "../logger"; import { TestFileResult } from "../api/workspace/test-result/file-result"; const log = createLogger("Results"); export type TestFileStatus = "IDLE" | "EXECUTING"; export interface CoverageSummary { statement: number; line: number; function: number; branch: number; } export default class Results { private projectRoot: string = ""; private results: { [path: string]: TestFileResult; } = {}; private testStatus: { [path: string]: { isExecuting: boolean; containsFailure: boolean; }; } = {}; private summary: { numFailedTests: number; numPassedTests: number; numPassedTestSuites: number; numFailedTestSuites: number; }; private coverage: CoverageSummary = { statement: 0, line: 0, function: 0, branch: 0 }; private haveCoverageReport: boolean = false; public coverageFilePath: string = ""; public coverageDirectory: string = ""; constructor(projectRoot: string) { this.projectRoot = projectRoot; this.results = {}; this.summary = { numFailedTests: 0, numPassedTests: 0, numPassedTestSuites: 0, numFailedTestSuites: 0 }; this.checkIfCoverageReportExists(); } public setTestStart(path: string) { if (!this.testStatus[path]) { this.testStatus[path] = { isExecuting: false, containsFailure: false }; } this.testStatus[path].isExecuting = true; } public setTestReport(path: string, report: any) { this.results[path] = report; this.testStatus[path].isExecuting = false; if (report.numFailingTests > 0) { this.testStatus[path].containsFailure = true; } else { this.testStatus[path].containsFailure = false; } } public getResult(path: string): TestFileResult | null { return this.results[path] || null; } public setSummary( passedTests: number, failedTests: number, numPassedTestSuites: number, numFailedTestSuites: number ) { this.summary = { numFailedTests: failedTests, numPassedTests: passedTests, numPassedTestSuites, numFailedTestSuites }; } public markExecutingAsStopped() { this.testStatus = Object.entries(this.testStatus).reduce( (acc, [key, value]) => ({ [key]: { ...value, isExecuting: false }, ...acc }), {} ); } public getSummary() { return this.summary; } public getFailedTests() { return Object.entries(this.testStatus) .filter(([path, status]) => { return status.containsFailure; }) .map(([path]) => path); } public getPassedTests() { return Object.entries(this.testStatus) .filter(([path, status]) => { return !status.containsFailure && !status.isExecuting; }) .map(([path]) => path); } public getExecutingTests() { return Object.entries(this.testStatus) .filter(([path, status]) => { return status.isExecuting === true; }) .map(([path]) => path); } public mapCoverage(data: any) { if (!data) { this.coverage = { statement: 0, branch: 0, function: 0, line: 0 }; return; } const sourceMapStore = createSourceMapStore(); const coverageMap = createCoverageMap(data); const transformed = sourceMapStore.transformCoverage(coverageMap); const coverageSummary = transformed.map.getCoverageSummary(); const statementCoverage = coverageSummary.statements.pct as any; const branchCoverage = coverageSummary.branches.pct as any; const functionCoverage = coverageSummary.functions.pct as any; const lineCoverage = coverageSummary.lines.pct as any; this.coverage = { statement: statementCoverage === "Unknown" ? 0 : statementCoverage, branch: branchCoverage === "Unknown" ? 0 : branchCoverage, function: functionCoverage === "Unknown" ? 0 : functionCoverage, line: lineCoverage === "Unknown" ? 0 : lineCoverage }; } public checkIfCoverageReportExists() { this.haveCoverageReport = existsSync(this.coverageFilePath); return this.haveCoverageReport; } public getCoverage() { return this.coverage; } public doesHaveCoverageReport() { return this.haveCoverageReport; } public getCoverageReportPath(config: MajesticConfig) { try { const configProcess = spawnSync( "node", [ config.jestScriptPath, ...(config.args || []), "--showConfig", "--json" ], { cwd: this.projectRoot, shell: true, stdio: "pipe", env: { CI: "true", ...(config.env || {}), ...process.env } } ); let filesStr = configProcess.stdout.toString().trim(); if (filesStr === "") { filesStr = configProcess.stderr.toString().trim(); } const defaultCoveragePath = join(this.projectRoot, "coverage"); const jestConfig = JSON.parse(filesStr); this.coverageDirectory = (jestConfig.globalConfig && jestConfig.globalConfig.coverageDirectory) || defaultCoveragePath; this.coverageFilePath = join( this.coverageDirectory, "/lcov-report/index.html" ); } catch (e) { log( `Error occured while obtaining Jest cofiguration for coverage report ${e.toString()}` ); } } } ================================================ FILE: server/services/types.ts ================================================ export interface DirectoryItem { name: string; path: string; type: "directory" | "file"; children?: DirectoryItem[]; } export interface TreeMap { [path: string]: { name: string; path: string; parent?: string; type: "directory" | "file"; }; } export interface MajesticConfig { jestScriptPath: string; args?: string[]; env?: { [key: string]: string }; } ================================================ FILE: server/static-files.ts ================================================ import * as exp from "express"; import { resolve, join } from "path"; import { pubsub } from "./event-emitter"; export function initializeStaticRoutes(express: exp.Application, root: string) { express.get("/", (req, res) => res.sendFile("./ui/index.html", { root: resolve(__dirname, "..") }) ); express.get("/ui.bundle.js", (req, res) => res.sendFile("./ui/ui.bundle.js", { root: resolve(__dirname, "..") }) ); express.get("/favicon.ico", (req, res) => res.sendFile("./ui/favicon.ico", { root: resolve(__dirname, "..") }) ); express.get("/logo.png", (req, res) => res.sendFile("./ui/logo.png", { root: resolve(__dirname, "..") }) ); pubsub.subscribe("WorkspaceInitialized", ({ coverageDirectory }) => { if (coverageDirectory && coverageDirectory.trim() !== "") { express.use("/coverage", exp.static(coverageDirectory)); } }); } ================================================ FILE: server/typings.d.ts ================================================ declare module "directory-tree"; declare module "micromatch"; declare module "@babel/traverse"; declare module "nanoid"; declare module "read-pkg-up"; declare module "open"; declare module "launch-editor"; declare module "*.json"; declare module "lodash.throttle"; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "es2015", "allowSyntheticDefaultImports": true, "target": "es5", "lib": ["es6", "dom", "es2017.object"], "sourceMap": true, "allowJs": true, "jsx": "react", "moduleResolution": "node", "forceConsistentCasingInFileNames": true, "noImplicitReturns": false, "noImplicitThis": true, "noImplicitAny": true, "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": false, "experimentalDecorators": true, "skipLibCheck": true, "emitDecoratorMetadata": true }, "exclude": [] } ================================================ FILE: tsconfig.server.json ================================================ { "compilerOptions": { "rootDir": "./server", "outDir": "./dist/server", "module": "commonjs", "target": "es2016", "lib": ["es6", "dom", "es2017.object", "esnext.asynciterable"], "sourceMap": true, "allowJs": true, "jsx": "react", "moduleResolution": "node", "forceConsistentCasingInFileNames": false, "noImplicitReturns": false, "noImplicitThis": true, "noImplicitAny": false, "strictNullChecks": false, "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": false, "experimentalDecorators": true, "emitDecoratorMetadata": true, "skipLibCheck": true, "newLine": "LF", "resolveJsonModule": true }, "include": ["server"] } ================================================ FILE: ui/apollo-client.ts ================================================ import { ApolloClient, HttpLink, InMemoryCache } from "apollo-client-preset"; import { WebSocketLink } from "apollo-link-ws"; import { getMainDefinition } from "apollo-utilities"; import { split } from "apollo-link"; declare var PRODUCTION: boolean; let WS_URL = "ws://localhost:4000"; let HTTP_URL = "http://localhost:4000"; if (PRODUCTION) { const WS_PROTOCOL = window.location.protocol === "https:" ? "wss:" : "ws:"; WS_URL = `${WS_PROTOCOL}//${window.location.host}`; HTTP_URL = `${window.location.protocol}//${window.location.host}`; } export function getAPIUrl() { return HTTP_URL; } const wsLink = new WebSocketLink({ uri: WS_URL, options: { reconnect: true } }); const httpLink = new HttpLink({ uri: HTTP_URL }); const link = split( ({ query }: any) => { const { kind, operation } = getMainDefinition(query); return kind === "OperationDefinition" && operation === "subscription"; }, wsLink, httpLink ); const client = new ApolloClient({ link, cache: new InMemoryCache() }); export default client; ================================================ FILE: ui/app.gql ================================================ { app { selectedFile } } ================================================ FILE: ui/app.tsx ================================================ import React, { useState } from "react"; import styled from "styled-components"; import SplitPane from "react-split-pane"; import { useQuery, useMutation } from "react-apollo-hooks"; import Sidebar from "./sidebar"; import TestFile from "./test-file"; import APP from "./app.gql"; import WORKSPACE from "./query.gql"; import useKeys from "./hooks/use-keys"; import useSubscription from "./test-file/use-subscription"; import SUMMARY_QUERY from "./summary-query.gql"; import SUMMARY_SUBS from "./summary-subscription.gql"; import RUNNER_STATUS_QUERY from "./runner-status-query.gql"; import RUNNER_STATUS_SUBS from "./runner-status-subs.gql"; import STOP_RUNNER from "./stop-runner.gql"; import { Search } from "./search"; import SET_SELECTED_FILE from "./set-selected-file.gql"; import { Workspace } from "../server/api/workspace/workspace"; import { color } from "styled-system"; import { RunnerStatus } from "../server/api/runner/status"; import { Summary } from "../server/api/workspace/summary"; import CoveragePanel from "./coverage-panel"; const ContainerDiv = styled.div` display: flex; flex-direction: row; width: 100%; `; const PlaceHolder = styled.div` display: flex; height: 100%; ${color} `; interface AppResult { app: { selectedFile: string }; } interface WorkspaceResult { workspace: Workspace; } export default function App() { const { data: { app: { selectedFile } }, refetch } = useQuery(APP); const { data: { workspace }, refetch: refetchFiles } = useQuery(WORKSPACE); const { data: summary }: { data: Summary } = useSubscription( SUMMARY_QUERY, SUMMARY_SUBS, {}, result => result.summary, result => result.changeToSummary, "Summary Sub" ); const { data: runnerStatus }: { data: RunnerStatus } = useSubscription( RUNNER_STATUS_QUERY, RUNNER_STATUS_SUBS, {}, result => result.runnerStatus, result => result.runnerStatusChange, "Runner subs" ); const setSelectedFile = useMutation(SET_SELECTED_FILE); const handleFileSelection = (path: string | null) => { if (path !== null) { setShowCoverage(false); } setSelectedFile({ variables: { path } }); refetch(); }; const stopRunner = useMutation(STOP_RUNNER); const [isSearchOpen, setSearchOpen] = useState(false); const keys = useKeys(); if (isSearchOpen && keys.has("Escape")) { setSearchOpen(false); } const [showCoverage, setShowCoverage] = useState(false); return ( { setSearchOpen(true); }} onRefreshFiles={() => { refetchFiles(); }} onStop={() => { stopRunner(); }} onShowCoverage={() => { setShowCoverage(!showCoverage); }} /> {showCoverage && } {selectedFile ? ( { stopRunner(); }} /> ) : ( )} setSearchOpen(false)} onItemClick={path => { handleFileSelection(path); setSearchOpen(false); }} /> ); } ================================================ FILE: ui/components/button.tsx ================================================ import React from "react"; import styled from "styled-components"; import { space, color, fontSize } from "styled-system"; const StyledButton = styled.button` display: flex; align-items: center; color: ${props => (props.minimal ? "#ffffff" : "#242326")}; text-align: center; transition: all 0.5s; border: 1px solid #ffd062; border-radius: 3px; background-color: ${props => (props.minimal ? "transparent" : "#FFD062")}; cursor: pointer; margin-right: 5px; padding: 6px; ${color}; ${fontSize}; &:hover { background-color: ${props => (props.bg ? props.bg : "#ffd062")}; } &:focus { outline: none; } `; const Spacer = styled.div` width: 5px; `; export default function Button(props: any) { return ( {props.icon} {props.icon && props.children && } {props.children} ); } ================================================ FILE: ui/container.tsx ================================================ import React, { Component, Suspense } from "react"; import { ApolloProvider as ApolloHooksProvider } from "react-apollo-hooks"; import { ApolloProvider } from "react-apollo"; import { ThemeProvider } from "styled-components"; import client from "./apollo-client"; import App from "./app"; import theme from "./theme"; import { createGlobalStyle } from "styled-components"; import splitPanelCSS from "./split-panel-style"; import "typeface-open-sans"; import Loading from "./loading"; import { ErrorBoundary } from "./error"; const GlobalStyle = createGlobalStyle` body { font-family: 'Open sans'; font-size: 13px; margin: 0px;} ${splitPanelCSS} `; export default class Container extends Component { render() { return ( }> ); } } ================================================ FILE: ui/coverage-panel/index.tsx ================================================ import React from "react"; import styled from "styled-components"; import { getAPIUrl } from "../apollo-client"; const Frame = styled.iframe` width: 100%; height: 100%; `; export default function CoveragePanel() { return ( ); } ================================================ FILE: ui/error.tsx ================================================ import React, { Component } from "react"; import styled from "styled-components"; const Container = styled.div` display: flex; flex-direction: column; width: 100%; height: 100vh; align-items: center; justify-content: center; background-color: #262529; color: #fdc055; font-size: 25px; font-weight: 500; `; const Loader = styled.div` margin-bottom: 20px; svg { text-align: center; margin: auto; width: 60px; height: 60px; } #icon-stop-circle .stopping { animation-name: stopping; animation-duration: 5s; animation-timing-function: ease-in-out; animation-iteration-count: infinite; transform-origin: center center; } @keyframes stopping { from, 50%, to { opacity: 1; fill: #ea3970; stroke: none; } 25%, 75% { opacity: 0; } } `; const Message = styled.div` font-size: 15px; `; export class ErrorBoundary extends Component { state = { didError: false }; componentDidCatch() { this.setState({ didError: true }); } render() { if (!this.state.didError) { return this.props.children; } return ( Oops, Something went wrong. Check the terminal for exact error message! ); } } ================================================ FILE: ui/hooks/use-keys.ts ================================================ import { useEffect, useState } from "react"; export function hasKeys(expectedKeys: string[], pressedKeys: Map ) { return expectedKeys.every(k => pressedKeys.has(k)); }; export default function useKeys() { const [keys, setKeys] = useState(new Map()); const hotKeys = ["Alt", "Enter", "Escape", "s", "t", "w"]; function downHandler({ key }:KeyboardEvent) { // only update state for keys we are watching if (hotKeys.includes(key)) { keys.set(key, true); // create a new Map object to guarantee that state updates setKeys(new Map(keys)); } } const upHandler = ({ key }:KeyboardEvent) => { if (hotKeys.includes(key)) { keys.delete(key); setKeys(new Map(keys)); } }; useEffect(() => { window.addEventListener("keydown", downHandler); window.addEventListener("keyup", upHandler); return () => { window.removeEventListener("keydown", downHandler); window.removeEventListener("keyup", upHandler); }; }, []); return keys; } ================================================ FILE: ui/index.tsx ================================================ import React from "react"; import ReactDOM from "react-dom"; import "@babel/polyfill"; import Container from "./container"; import "react-tippy/dist/tippy.css"; ReactDOM.render(, document.getElementById("root")); if ((module as any).hot) { (module as any).hot.accept("./container", () => { const NextApp = require("./container").default; ReactDOM.render(, document.getElementById("root")); }); } ================================================ FILE: ui/loading.tsx ================================================ import React from "react"; import styled from "styled-components"; const Container = styled.div` display: flex; flex-direction: column; width: 100%; height: 100vh; align-items: center; justify-content: center; background-color: #262529; color: #fdc055; font-size: 25px; font-weight: 500; `; const Loader = styled.div` margin-bottom: 20px; svg { text-align: center; margin: auto; width: 60px; height: 60px; } #icon-crop-button { animation: cropped 1s alternate infinite ease-in-out; transform-origin: center; fill: aliceblue; } @-webkit-keyframes cropped { 0% { transform: rotate(0deg) scale(1); } 50% { transform: rotate(90deg) scale(0.9); } 100% { transform: rotate(180deg) scale(1); } } @keyframes cropped { 0% { transform: rotate(0deg) scale(1); } 50% { transform: rotate(90deg) scale(0.9); } 100% { transform: rotate(180deg) scale(1); } } `; const Message = styled.div` font-size: 15px; `; export default function Loading() { return ( Getting things ready for you ); } ================================================ FILE: ui/query.gql ================================================ { workspace { projectRoot name files { path name type parent } } } ================================================ FILE: ui/runner-status-query.gql ================================================ { runnerStatus { running activeFile watching } } ================================================ FILE: ui/runner-status-subs.gql ================================================ subscription { runnerStatusChange { running activeFile watching } } ================================================ FILE: ui/search/index.tsx ================================================ import React, { useEffect, useRef, useState } from "react"; import styled from "styled-components"; import { Item } from "../../server/api/workspace/tree"; import { color } from "styled-system"; const Drop = styled.div` position: absolute; background-color: #444444; opacity: 0.7; top: 0; left: 0; right: 0; bottom: 0px; z-index: 1; `; const Container = styled.div` width: 700px; max-height: 500px; position: absolute; z-index: 1; margin-left: auto; margin-right: auto; left: 0; right: 0; top: 150px; padding: 20px; border-radius: 4px; display: flex; flex-direction: column; ${color}; `; const ItemContainer = styled.div` display: flex; padding: 5px; cursor: pointer; color: #fefefe; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-radius: 1px; min-height: 20px; &:hover { background-color: #404148; } `; const ResultContainer = styled.div` display: flex; flex-direction: column; overflow: auto; `; const SearchBox = styled.input` padding: 5px; border: none; width: 99%; margin-bottom: 15px; padding: 5px; font-size: 13px; border-radius: 2px; &:focus { outline: none; } `; interface Props { projectRoot: string; show: boolean; files: Item[]; onItemClick: (path: string) => void; onClose: () => void; } export function Search({ projectRoot, files, show, onItemClick, onClose }: Props) { const onlyFiles = files.filter(file => file.type === "file"); const [query, setQuery] = useState(""); const searchBoxRef = useRef(null); useEffect(() => { if (searchBoxRef && searchBoxRef.current) { searchBoxRef.current.focus(); } }, [show]); if (!show) return null; return ( { setQuery(event.target.value); }} /> {onlyFiles .filter(file => file.path.toLowerCase().includes(query.toLowerCase()) ) .map((file: any, index: number) => ( { onItemClick(file.path); }} > {file.path.toLowerCase().replace(projectRoot.toLowerCase(), "")} ))} ); } ================================================ FILE: ui/set-selected-file.gql ================================================ mutation SetSelectedFile($path: String) { setSelectedFile(path: $path) { selectedFile } } ================================================ FILE: ui/sidebar/execution-indicator.tsx ================================================ import React from "react"; export default function ExecutionIndicator() { return ( ); } ================================================ FILE: ui/sidebar/file-item.tsx ================================================ import React, { memo } from "react"; import styled from "styled-components"; import { File, Folder, ChevronRight, ChevronDown, Frown, ZapOff, } from "react-feather"; import { color } from "styled-system"; import { TreeNode } from "./transformer"; import ExecutionIndicator from "./execution-indicator"; const Container = styled.div` display: flex; flex-direction: column; margin-left: 20px; ${color}; `; const Content = styled.div` display: flex; align-items: center; padding: 2.5px; cursor: pointer; color: ${(props) => props.failed ? "#FE5339" : props.passing ? "#19E28D" : null}; background-color: ${(props) => (props.selected ? "#444444" : null)}; border-radius: 3px; margin-bottom: 2px; font-weight: 600; &:hover { background-color: #444444; } `; const Label = styled.div` margin-left: 5px; font-size: 12px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; `; const EmptyChevron = styled.div` width: 5px; `; const ExecutionWrapper = styled.div``; interface Props { item: TreeNode; style: any; selectedFile: string; setSelectedFile: (path: string) => void; onToggle: (path: string, isCollapsed: boolean) => void; } function FileItem({ item, selectedFile, setSelectedFile, onToggle, style, }: Props) { const Icon = item.type === "directory" ? Folder : item.haveFailure ? ZapOff : File; let Chevron: any = EmptyChevron; if (item.type === "directory") { Chevron = item.isCollapsed ? ChevronRight : ChevronDown; } const handleClick = () => { if (item.type === "file") { setSelectedFile(item.path); } if (item.type === "directory") { onToggle(item.path, !item.isCollapsed); } }; return ( {!item.isExecuting && } {item.isExecuting && ( )} ); } export default memo(FileItem, (pre: Props, next: Props) => { return ( pre.item.isExecuting === next.item.isExecuting && pre.item.isCollapsed === next.item.isCollapsed && pre.selectedFile === next.selectedFile ); }); ================================================ FILE: ui/sidebar/index.tsx ================================================ import React, { useState } from "react"; import styled from "styled-components"; import { useMutation, useQuery } from "react-apollo-hooks"; import { space, color } from "styled-system"; import { Tooltip } from "react-tippy"; import SET_WATCH_MODE from "./set-watch-mode.gql"; import SHOULD_COLLECT_COVERAGE from "./should-collect-coverage.gql"; import SET_COLLECT_COVERAGE from "./set-collect-coverage.gql"; import { Workspace } from "../../server/api/workspace/workspace"; import { transform, filterFailure } from "./transformer"; import Summary from "./summary"; import { Summary as SummaryType } from "../../server/api/workspace/summary"; import RUN from "./run.gql"; import useKeys, { hasKeys } from "../hooks/use-keys"; import { Play, Eye, Search, RefreshCw, ZapOff, StopCircle, FileText, Layers, ChevronDown, ChevronRight } from "react-feather"; import Button from "../components/button"; import { RunnerStatus } from "../../server/api/runner/status"; import Tree from "./tree"; import Logo from "./logo"; const Container = styled.div` ${space}; ${color}; height: 100vh; `; const ActionsPanel = styled.div` ${space} display: flex; justify-content: space-between; `; const RightActionPanel = styled.div` display: flex; `; const FileHeader = styled.div` ${space} display: flex; justify-content: space-between; align-items: center; font-size: 13px; `; const FilesHeader = styled.div` font-weight: 400; font-size: 11px; `; const RightFilesAction = styled.div` display: flex; `; interface Props { selectedFile: string; workspace: Workspace; summary: SummaryType | undefined; runnerStatus?: RunnerStatus; showCoverage: boolean; onSelectedFileChange: (path: string) => void; onSearchOpen: () => void; onRefreshFiles: () => void; onStop: () => void; onShowCoverage: () => void; } export default function TestExplorer ({ selectedFile, workspace, onSelectedFileChange, summary, showCoverage, runnerStatus, onSearchOpen, onRefreshFiles, onStop, onShowCoverage }: Props) { const failedItems = (summary && summary.failedTests) || []; const executingItems = (summary && summary.executingTests) || []; const passingTests = (summary && summary.passingTests) || []; const run = useMutation(RUN); const [collapsedItems, setCollapsedItems] = useState({}); const handleFileToggle = (path: string, isCollapsed: boolean) => { setCollapsedItems({ ...collapsedItems, [path]: isCollapsed }); }; const [showFailedTests, setShowFailedTests] = useState(false); const items = workspace.files; const root = items[0]; let files = transform( root as any, executingItems, failedItems, passingTests, collapsedItems, showFailedTests, items ); const onCollapseAll = () => { const newCollapsedItems = {}; files.forEach(file => { if (file.type === "directory" && file.parent) { newCollapsedItems[file.path] = true; } }); setCollapsedItems(newCollapsedItems) } const onExpandAll = () => { setCollapsedItems({}) } if (showFailedTests && failedItems.length) { files = filterFailure(files); } const { data: { shouldCollectCoverage }, refetch: refetchCoverageFlag } = useQuery(SHOULD_COLLECT_COVERAGE); const setCollectCoverage = useMutation(SET_COLLECT_COVERAGE); const handleFileSelection = (path: string) => { onSelectedFileChange(path); }; const setWatchMode = useMutation(SET_WATCH_MODE); const handleSetWatchModel = (watch: boolean) => { setWatchMode({ variables: { watch } }); }; const isRunning = runnerStatus && runnerStatus.running; const keys = useKeys(); if (hasKeys(["Alt", "t"], keys)) { run(); } else if (hasKeys(["Alt", "w"], keys)) { if (runnerStatus) { handleSetWatchModel(!runnerStatus.watching); } } else if (hasKeys(["Alt", "s"], keys)) { onSearchOpen(); } return ( Tests {summary && summary.failedTests && summary.failedTests.length > 0 && ( )} {!showFailedTests && ( )} {!showFailedTests && ( )} {summary && summary.haveCoverageReport && ( )} ); } ================================================ FILE: ui/sidebar/logo.tsx ================================================ import React from "react"; import styled from "styled-components"; import logo from "../assets/logo.png"; const Container = styled.div` font-size: 25px; text-align: center; margin-bottom: 15px; `; export default function Logo() { return ( ); } ================================================ FILE: ui/sidebar/run.gql ================================================ mutation { run } ================================================ FILE: ui/sidebar/set-collect-coverage.gql ================================================ mutation SetCollectCoverage($collect: Boolean!) { setCollectCoverage(collect: $collect) } ================================================ FILE: ui/sidebar/set-watch-mode.gql ================================================ mutation SetWatchMode($watch: Boolean!) { toggleWatch(watch: $watch) { watching } } ================================================ FILE: ui/sidebar/should-collect-coverage.gql ================================================ { shouldCollectCoverage } ================================================ FILE: ui/sidebar/summary/index.tsx ================================================ import React from "react"; import styled from "styled-components"; import { space } from "styled-system"; import { useSpring, animated } from "react-spring"; import { CheckCircle, ZapOff, Layers } from "react-feather"; import { Summary } from "../../../server/api/workspace/summary"; const Container = styled.div` ${space}; `; const Row = styled.div` display: flex; font-size: 16px; margin-bottom: 5px; `; const Cell = styled.div` display: flex; flex-direction: column; flex-grow: 1; `; const Label = styled.div` font-size: 12px; color: #dcdbdb; `; const Value = styled.div` font-size: 20px; color: ${props => (props.failed ? "#FF4F56" : "#19E28D")}; `; const CoverageLabel = styled.div` font-size: 10px; color: #dcdbdb; `; const CoverageValue = styled.div` font-size: 14px; `; const Coverage = styled.div` margin-top: 10px; `; interface Props { summary: Summary | undefined; } export default function SummaryPanel({ summary }: Props) { const passedSuitesProps = useSpring({ number: summary && summary.numPassedTestSuites | 0, from: { number: 0 } } as any); const failedSuitesProps = useSpring({ number: summary && summary.numFailedTestSuites | 0, from: { number: 0 } } as any); const passedTestProps = useSpring({ number: summary && summary.numPassedTests | 0, from: { number: 0 } } as any); const failedTestProps = useSpring({ number: summary && summary.numFailedTests | 0, from: { number: 0 } } as any); const coverage = summary && summary.coverage; const haveCoverage = coverage && (coverage.branch || coverage.function || coverage.line || coverage.statement); return ( {(passedSuitesProps as any).number.interpolate((value: any) => value.toFixed() )} {(failedSuitesProps as any).number.interpolate((value: any) => value.toFixed() )} {(passedTestProps as any).number.interpolate((value: any) => value.toFixed() )} {(failedTestProps as any).number.interpolate((value: any) => value.toFixed() )} {!!haveCoverage && ( {summary && summary.coverage && summary.coverage.statement}% Stmts {summary && summary.coverage && summary.coverage.branch}% Branch {summary && summary.coverage && summary.coverage.function}% Funcs {summary && summary.coverage && summary.coverage.line}% Lines )} ); } ================================================ FILE: ui/sidebar/transformer.ts ================================================ import { Item } from "../../server/api/workspace/tree"; export interface TreeNode extends Item { name: string; path: string; isCollapsed: boolean; haveFailure: boolean; passing: boolean; isExecuting: boolean; hierarchy: number; } export function transform( item: TreeNode, executingTests: string[], failedFiles: string[], passingTests: string[], collapsedFiles: { [path: string]: boolean }, showFailedTests: boolean, items: Item[], results: TreeNode[] = [], hierarchy = 0 ) { const isCollapsed = collapsedFiles[item.path] && !showFailedTests; // when showing failed tests, keep all expanded const haveFailure = failedFiles.indexOf(item.path) > -1; const nextChildren = getChildren(item.path, items); const treeItem = { type: item.type, name: item.name, path: item.path, parent: item.parent, hierarchy: hierarchy, isCollapsed: isCollapsed, passing: passingTests.indexOf(item.path) > -1, haveFailure, isExecuting: executingTests.indexOf(item.path) > -1 }; results.push(treeItem); if (!isCollapsed) { nextChildren.forEach(item => { transform( item as any, executingTests, failedFiles, passingTests, collapsedFiles, showFailedTests, items, results, hierarchy + 1 ); }); } return results; } export const filterFailure = (results: TreeNode[]) => { const finalResults = []; for (let i = results.length - 1; i >= 0; i--) { const item = results[i]; if (item.type === "file" && item.haveFailure === true) { finalResults.push(item); } else if (item.type === "directory") { const hasFailedChildren = haveFailedChildren(item.path, finalResults); if (hasFailedChildren) { finalResults.push(item); } } } return finalResults.reverse(); }; function haveFailedChildren(path: string, results: TreeNode[]) { return ( results.filter( result => result.parent === path && (result.haveFailure === true || result.type === "directory") ).length > 0 ); } function sortAsc(a: Item, b: Item){ return a.name > b.name ? 1 : -1; } function getChildren(path: string, files: Item[]) { const fileList = files.filter(file => file.parent === path); return fileList.sort(sortAsc); } ================================================ FILE: ui/sidebar/tree.tsx ================================================ import React from "react"; import styled from "styled-components"; import { FixedSizeList as List } from "react-window"; import AutoResizer from "react-virtualized-auto-sizer"; import FileItem from "./file-item"; import { TreeNode } from "./transformer"; const FileTreeContainer = styled.div` overflow: auto; height: calc(100vh - 173px); margin-left: -20px; `; interface Props { results: TreeNode[]; selectedFile: string; onFileSelection: (path: string) => void; onToggle: (path: string, isCollapsed: boolean) => void; } export default function Tree({ results, selectedFile, onFileSelection, onToggle }: Props) { return ( {({ height, width }: any) => { return ( {({ index, style }: any) => ( )} ); }} ); } ================================================ FILE: ui/split-panel-style.ts ================================================ const splitPanelCSS = ` .Resizer { background: #404148; opacity: .8; z-index: 1; box-sizing: border-box; background-clip: padding-box; } .Resizer:hover { transition: all 2s ease; } .Resizer.horizontal { height: 11px; margin: -5px 0; border-top: 5px solid rgba(255, 255, 255, 0); border-bottom: 5px solid rgba(255, 255, 255, 0); cursor: row-resize; width: 100%; } .Resizer.horizontal:hover { border-top: 5px solid rgba(0, 0, 0, 0.5); border-bottom: 5px solid rgba(0, 0, 0, 0.5); } .Resizer.vertical { width: 11px; margin: 0 -5px; border-left: 3px solid rgba(255, 255, 255, 0.0); border-right: 3px solid rgba(255, 255, 255, 0.0); cursor: col-resize; } .Resizer.vertical:hover { border-left: 3px solid rgba(0, 0, 0, 0.5); border-right: 3px solid rgba(0, 0, 0, 0.5); } .Resizer.disabled { cursor: not-allowed; } .Resizer.disabled:hover { border-color: transparent; } `; export default splitPanelCSS; ================================================ FILE: ui/stop-runner.gql ================================================ mutation { stop } ================================================ FILE: ui/summary-query.gql ================================================ query { summary { numPassedTests numFailedTests numPassedTestSuites numFailedTestSuites failedTests executingTests passingTests coverage { statement function branch line } haveCoverageReport } } ================================================ FILE: ui/summary-subscription.gql ================================================ subscription { changeToSummary { numPassedTests numFailedTests numPassedTestSuites numFailedTestSuites failedTests executingTests passingTests coverage { statement function branch line } haveCoverageReport } } ================================================ FILE: ui/test-file/console-panel/index.tsx ================================================ import React from "react"; import styled from "styled-components"; import { ObjectInspector, chromeDark } from "react-inspector"; import { ConsoleLog } from "../../../server/api/workspace/test-result/console-log"; import { AlertCircle, XCircle, MessageSquare } from "react-feather"; const Container = styled.div` display: flex; flex-direction: column; padding: 10px; background-color: #404148; border-radius: 5px; margin-bottom: 10px; `; const Header = styled.div` font-size: 11px; color: white; margin-bottom: 5px; `; const Content = styled.pre` display: flex; margin-bottom: 3px; font-size: 12px; border-radius: 3px; padding: 4px; font-family: monospace; `; const Logs = styled.div` display: flex; flex-direction: column; max-height: 300px; overflow: auto; `; const IconWrapper = styled.div` margin-right: 5px; margin-top: 1px; `; const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, ""); function getIcon(type: String) { let icon = null; switch (type) { case "warn": icon = ; break; case "error": icon = ; break; case "log": icon = ; break; } return {icon}; } interface Props { consoleLogs: ConsoleLog[]; } export default function ConsolePanel({ consoleLogs }: Props) { return (
Console logs from the file
{consoleLogs.map((log, index) => { let result = log.message; try { result = eval("(" + log.message + ")"); } catch (e) { console.log(e); } if (typeof result === "string") { return ( {getIcon(log.type)} {cleanAnsiCodes(result)} ); } return ( {getIcon(log.type)} ); })}
); } ================================================ FILE: ui/test-file/error-panel/index.tsx ================================================ import React from "react"; import styled from "styled-components"; import * as Convert from "ansi-to-html"; const convert = new Convert({ colors: { 1: "#FF4F56", 2: "#19E28D" } }); const Container = styled.div` padding: 10px; background-color: #404148; border-radius: 5px; margin-bottom: 10px; `; interface Props { failureMessage: string; } function escapeHtml(unsafe: string) { return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } export default function ErrorPanel({ failureMessage }: Props) { if (!failureMessage || failureMessage.trim() === "") { return null; } return (
    
  );
}


================================================
FILE: ui/test-file/file-items-subscription.gql
================================================
subscription($path: String!) {
  fileChange(path: $path) {
    items {
      id
      name
      type
      parent
      only
    }
  }
}


================================================
FILE: ui/test-file/index.tsx
================================================
import React, { memo } from "react";
import styled from "styled-components";
import { space, color } from "styled-system";
import { useMutation } from "react-apollo-hooks";
import FILEITEMS_SUB from "./file-items-subscription.gql";
import FILEITEMS from "./query.gql";
import RUNFILE from "./run-file.gql";
import UPDATE_SNAPSHOT from "./update-snapshot.gql";
import FILERESULTSUB from "./subscription.gql";
import RESULT from "./result.gql";
import Test from "./test-item";
import { transform } from "./transformer";
import useSubscription from "./use-subscription";
import FileSummary from "./summary";
import { TestFileResult } from "../../server/api/workspace/test-result/file-result";
import { TestFile as TestFileModel } from "../../server/api/workspace/test-file";
import ConsolePanel from "./console-panel";
import ErrorPanel from "./error-panel";
import useKeys, { hasKeys } from "../hooks/use-keys";

const Container = styled.div`
  ${space};
  ${color};
  height: 100vh;
  padding-left: 20px;
`;

const Content = styled.div`
  overflow: auto;
  height: calc(100vh - 118px);

  ${({ dim }: any) => dim && `
  opacity: .5;
  `}
`;

const TestItemsContainer = styled.div`
  margin-left: -25px;
`;

interface Props {
  selectedFilePath: string;
  isRunning: boolean;
  projectRoot: string;
  onStop: () => void;
}

function TestFile({ selectedFilePath, isRunning, projectRoot, onStop }: Props) {
  const { data: fileItemResult }: { data: TestFileModel } = useSubscription(
    FILEITEMS,
    FILEITEMS_SUB,
    {
      path: selectedFilePath
    },
    result => result.file,
    result => result.fileChange
  );

  const suiteCount = ((fileItemResult && fileItemResult.items) || []).filter(
    fileItem => fileItem.type === "describe"
  ).length;

  const testCount = ((fileItemResult && fileItemResult.items) || []).filter(
    fileItem => fileItem.type === "it"
  ).length;

  const todoCount = ((fileItemResult && fileItemResult.items) || []).filter(
    fileItem => fileItem.type === "todo"
  ).length;

  const runFile = useMutation(RUNFILE, {
    variables: {
      path: selectedFilePath
    }
  });

  const updateSnapshot = useMutation(UPDATE_SNAPSHOT, {
    variables: {
      path: selectedFilePath
    }
  });

  const {
    data: result,
    loading
  }: { data: TestFileResult; loading: boolean } = useSubscription(
    RESULT,
    FILERESULTSUB,
    {
      path: selectedFilePath
    },
    result => result.result,
    result => result.changeToResult
  );

  const isUpdating = isRunning && (result ===  null ||(result.numPassingTests === 0 && result.numFailingTests === 0));

  const roots = (fileItemResult.items || []).filter(
    item => item.parent === null
  );
  const keys = useKeys();
  if (hasKeys(["Alt", "Enter"], keys)) {
    runFile();
  }
  return (
    
       {
          runFile();
        }}
        onStop={onStop}
        onSnapshotUpdate={() => {
          updateSnapshot();
        }}
      />
      
        {result && result.testResults && result.testResults.length === 0 && (
          
        )}
        {result && result.consoleLogs && result.consoleLogs.length > 0 && (
          
        )}
        {fileItemResult && (
          
            {roots.map(item => {
              const tree = transform(
                item as any,
                fileItemResult.items as any,
                0
              ) as any;
              return ;
            })}
          
        )}
      
    
  );
}

export default memo(TestFile, (pre: Props, next: Props) => {
  return (
    pre.isRunning === next.isRunning &&
    pre.selectedFilePath === next.selectedFilePath
  );
});


================================================
FILE: ui/test-file/open-failure.gql
================================================
mutation OpenFailure($failure: String!) {
  openFailure(failure: $failure)
}


================================================
FILE: ui/test-file/query.gql
================================================
query FileItems($path: String!) {
  file(path: $path) {
    items {
      id
      name
      type
      parent
      only
    }
  }
}


================================================
FILE: ui/test-file/result.gql
================================================
query Results($path: String!) {
  result(path: $path) {
    path
    numFailingTests
    numPassingTests
    failureMessage
    testResults {
      title
      numPassingAsserts
      status
      failureMessages
      ancestorTitles
      duration
    }
    consoleLogs {
      message
      type
      origin
    }
  }
}


================================================
FILE: ui/test-file/run-file.gql
================================================
mutation RunFile($path: String!) {
  runFile(path: $path)
}


================================================
FILE: ui/test-file/subscription.gql
================================================
subscription Results($path: String!) {
  changeToResult(path: $path) {
    path
    numFailingTests
    numPassingTests
    failureMessage
    testResults {
      title
      numPassingAsserts
      status
      failureMessages
      ancestorTitles
      duration
    }
    consoleLogs {
      message
      type
      origin
    }
  }
}


================================================
FILE: ui/test-file/summary/index.tsx
================================================
import React from "react";
import styled from "styled-components";
import { space, fontSize, color } from "styled-system";
import { useSpring, animated } from "react-spring";
import {
  Folder,
  Code,
  Play,
  StopCircle,
  Camera,
  CheckCircle,
  Frown,
  ZapOff,
  Circle,
  Eye
} from "react-feather";
import Button from "../../components/button";
import OPEN_IN_EDITOR from "./open-in-editor.gql";
import OPEN_SNAP_IN_EDITOR from "./open-snap-in-editor.gql";
import { Tooltip } from "react-tippy";
import { useMutation } from "react-apollo-hooks";

const Container = styled.div`
  position: relative;
  ${space};
  ${color};
  border-radius: 3px;
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
  overflow: hidden;
  flex-wrap: wrap;
`;

const ContainerBG = styled(animated.div)`
  @keyframes MOVE-BG {
    from {
      transform: translateX(0);
    }
    to {
      transform: translateX(27px);
    }
  }
  border-radius: 3px;
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  left: -46px;
  background: repeating-linear-gradient(
    45deg,
    #404148,
    #404148 10px,
    #242326 10px,
    #242326 20px
  );

  animation-name: MOVE-BG;
  animation-duration: 0.5s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
`;

const RightContainer = styled.div`
  z-index: 1;
`;

const InfoContainer = styled.div`
  display: flex;
`;

const Info = styled.div`
  display: flex;
  align-items: center;
  margin-right: 15px;
  font-weight: 600;
  ${color}
`;

const InfoLabel = styled.div`
  margin-left: 5px;
`;

const FilePath = styled.div`
  ${fontSize};
  ${space};
  word-break: break-all;
  font-weight: 600;
  margin-right: 5px;
`;

const ActionPanel = styled.div`
  display: flex;
  align-items: center;
  z-index: 1;
`;

const LoadingResult = styled.div`
  color: #d9eef2;
  margin-right: 10px;
  font-size: 12px;
`;

interface Props {
  path: string;
  projectRoot: string;
  suiteCount: number;
  testCount: number;
  todoCount: number;
  passingTests: number;
  failingTests: number;
  isRunning: boolean;
  isUpdating: boolean;
  isLoadingResult: boolean;
  onRun: () => void;
  onStop: () => void;
  onSnapshotUpdate: () => void;
  haveSnapshotFailures: boolean;
}

export default function FileSummary({
  path,
  projectRoot,
  suiteCount,
  testCount,
  todoCount,
  passingTests,
  failingTests,
  isRunning,
  isUpdating,
  isLoadingResult,
  onRun,
  onStop,
  onSnapshotUpdate,
}: Props) {
  const Icon = isRunning ? StopCircle : Play;

  const openInEditor = useMutation(OPEN_IN_EDITOR, {
    variables: {
      path
    }
  });

  const openSnapshotInEditor = useMutation(OPEN_SNAP_IN_EDITOR, {
    variables: {
      path
    }
  });

  return (
    
      {( isUpdating || isLoadingResult) && }
      
        
          {path.replace(projectRoot, "")}
        

        
          
             {suiteCount} Suites
          
          
             {testCount} Tests
          
          
            {" "}
            {passingTests} Passing tests
          
          
            {" "}
            {failingTests} Failing tests
          
        
      
      
        {isLoadingResult && Loading test results}