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
================================================
<div align="center">
<img src="./image.png" />
<br />
<br />
<a href="https://github.com/Raathigesh/majestic/actions">
<img src="https://img.shields.io/github/workflow/status/Raathigesh/majestic/Node%20CI?style=flat-square" />
</a>
<img src="https://img.shields.io/github/license/Raathigesh/majestic.svg?style=flat-square" />
<img src="https://img.shields.io/npm/v/majestic.svg?style=flat-square" />
<a href="https://spectrum.chat/majestic">
<img alt="Join the community on Spectrum" src="https://withspectrum.github.io/badge/badge.svg" />
</a>
</div>
<br />
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)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://www.duncanbeevers.com"><img src="https://avatars0.githubusercontent.com/u/7367?v=4" width="100px;" alt=""/><br /><sub><b>Duncan Beevers</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=duncanbeevers" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/M4cs"><img src="https://avatars3.githubusercontent.com/u/34947910?v=4" width="100px;" alt=""/><br /><sub><b>Max Bridgland</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=M4cs" title="Documentation">📖</a> <a href="#ideas-M4cs" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/Raathigesh/majestic/issues?q=author%3AM4cs" title="Bug reports">🐛</a> <a href="https://github.com/Raathigesh/majestic/commits?author=M4cs" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/yurm04"><img src="https://avatars0.githubusercontent.com/u/4642404?v=4" width="100px;" alt=""/><br /><sub><b>Yuraima Estevez</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=yurm04" title="Code">💻</a></td>
<td align="center"><a href="http://jake.nz"><img src="https://avatars2.githubusercontent.com/u/437471?v=4" width="100px;" alt=""/><br /><sub><b>Jake Crosby</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=jake-nz" title="Code">💻</a></td>
<td align="center"><a href="http://gavinhenderson.me"><img src="https://avatars1.githubusercontent.com/u/1359202?v=4" width="100px;" alt=""/><br /><sub><b>Gavin Henderson</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=gavinhenderson" title="Code">💻</a></td>
<td align="center"><a href="https://briwa.github.io"><img src="https://avatars1.githubusercontent.com/u/8046636?v=4" width="100px;" alt=""/><br /><sub><b>briwa</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=briwa" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Luanf"><img src="https://avatars0.githubusercontent.com/u/9099705?v=4" width="100px;" alt=""/><br /><sub><b>Luan Ferreira</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=Luanf" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/cse-tushar"><img src="https://avatars3.githubusercontent.com/u/12570521?v=4" width="100px;" alt=""/><br /><sub><b>Tushar Gupta</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=cse-tushar" title="Code">💻</a></td>
<td align="center"><a href="https://agu.st/"><img src="https://avatars3.githubusercontent.com/u/6601142?v=4" width="100px;" alt=""/><br /><sub><b>Agusti Fernandez</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=agustif" title="Code">💻</a> <a href="#ideas-agustif" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="http://blog.42at.com"><img src="https://avatars2.githubusercontent.com/u/233047?v=4" width="100px;" alt=""/><br /><sub><b>Moos</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/issues?q=author%3Amoos" title="Bug reports">🐛</a> <a href="https://github.com/Raathigesh/majestic/commits?author=moos" title="Code">💻</a> <a href="https://github.com/Raathigesh/majestic/commits?author=moos" title="Documentation">📖</a></td>
<td align="center"><a href="http://maciejzelek.space"><img src="https://avatars3.githubusercontent.com/u/25805810?v=4" width="100px;" alt=""/><br /><sub><b>MacZel</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=MacZel" title="Code">💻</a> <a href="#ideas-MacZel" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/krazylegz"><img src="https://avatars2.githubusercontent.com/u/36250?v=4" width="100px;" alt=""/><br /><sub><b>Vikram Dighe</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=krazylegz" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jsmey"><img src="https://avatars2.githubusercontent.com/u/10177710?v=4" width="100px;" alt=""/><br /><sub><b>John Smey</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=jsmey" title="Code">💻</a> <a href="#ideas-jsmey" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/Raathigesh/majestic/issues?q=author%3Ajsmey" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/BuckAMayzing"><img src="https://avatars2.githubusercontent.com/u/19292614?v=4" width="100px;" alt=""/><br /><sub><b>BuckAMayzing</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=BuckAMayzing" title="Code">💻</a> <a href="https://github.com/Raathigesh/majestic/issues?q=author%3ABuckAMayzing" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center"><a href="http://rahulakrishna.github.io"><img src="https://avatars2.githubusercontent.com/u/10240002?v=4" width="100px;" alt=""/><br /><sub><b>Rahul A. Krishna</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=rahulakrishna" title="Code">💻</a> <a href="#ideas-rahulakrishna" title="Ideas, Planning, & Feedback">🤔</a> <a href="#tool-rahulakrishna" title="Tools">🔧</a></td>
<td align="center"><a href="https://amilajack.com"><img src="https://avatars1.githubusercontent.com/u/6374832?v=4" width="100px;" alt=""/><br /><sub><b>Amila Welihinda</b></sub></a><br /><a href="#infra-amilajack" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/gregveres"><img src="https://avatars2.githubusercontent.com/u/12899823?v=4" width="100px;" alt=""/><br /><sub><b>gregveres</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/issues?q=author%3Agregveres" title="Bug reports">🐛</a> <a href="https://github.com/Raathigesh/majestic/commits?author=gregveres" title="Code">💻</a></td>
<td align="center"><a href="http://adamklein.dev"><img src="https://avatars3.githubusercontent.com/u/889418?v=4" width="100px;" alt=""/><br /><sub><b>adam klein</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=adamkleingit" title="Tests">⚠️</a> <a href="https://github.com/Raathigesh/majestic/commits?author=adamkleingit" title="Code">💻</a></td>
<td align="center"><a href="http://www.raphaelbarbazza.com"><img src="https://avatars1.githubusercontent.com/u/42906704?v=4" width="100px;" alt=""/><br /><sub><b>Raphaël Barbazza</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=rbarbazz" title="Code">💻</a></td>
<td align="center"><a href="https://philalsford.com"><img src="https://avatars3.githubusercontent.com/u/8849355?v=4" width="100px;" alt=""/><br /><sub><b>Phil Alsford</b></sub></a><br /><a href="https://github.com/Raathigesh/majestic/commits?author=philals" title="Documentation">📖</a></td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
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
================================================
/// <reference types="Cypress" />
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`] = `
<div
className="App"
>
Hello world
</div>
`;
================================================
FILE: integration/projects/basic/app.js
================================================
import React, { Component } from 'react';
class App extends Component {
render() {
return <div className="App">Hello world 123</div>;
}
}
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(<App />).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 <some text> (<file path>)
// it captures <file path> 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<TestFileResult> {
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<Summary> {
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<TestItem[]> {
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<any>`
display: flex;
height: 100%;
${color}
`;
interface AppResult {
app: { selectedFile: string };
}
interface WorkspaceResult {
workspace: Workspace;
}
export default function App() {
const {
data: {
app: { selectedFile }
},
refetch
} = useQuery<AppResult>(APP);
const {
data: { workspace },
refetch: refetchFiles
} = useQuery<WorkspaceResult>(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 (
<ContainerDiv>
<SplitPane
defaultSize={"calc(100% - 300px)"}
split="vertical"
primary="second"
pane1Style={{ minWidth: "300px" }}
pane2Style={{ maxWidth: "calc(100% - 300px)" }}
>
<Sidebar
workspace={workspace}
selectedFile={selectedFile}
onSelectedFileChange={handleFileSelection}
summary={summary}
runnerStatus={runnerStatus}
showCoverage={showCoverage}
onSearchOpen={() => {
setSearchOpen(true);
}}
onRefreshFiles={() => {
refetchFiles();
}}
onStop={() => {
stopRunner();
}}
onShowCoverage={() => {
setShowCoverage(!showCoverage);
}}
/>
{showCoverage && <CoveragePanel />}
{selectedFile ? (
<TestFile
projectRoot={workspace.projectRoot}
selectedFilePath={selectedFile}
isRunning={
(runnerStatus.running &&
runnerStatus.activeFile === selectedFile) ||
((summary && summary.executingTests) || []).includes(selectedFile)
}
onStop={() => {
stopRunner();
}}
/>
) : (
<PlaceHolder bg="dark" />
)}
</SplitPane>
<Search
projectRoot={workspace.projectRoot}
show={isSearchOpen}
files={workspace.files}
onClose={() => setSearchOpen(false)}
onItemClick={path => {
handleFileSelection(path);
setSearchOpen(false);
}}
/>
</ContainerDiv>
);
}
================================================
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<any>`
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 (
<StyledButton fontSize={12} {...props}>
{props.icon}
{props.icon && props.children && <Spacer />}
{props.children}
</StyledButton>
);
}
================================================
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 (
<React.Fragment>
<GlobalStyle />
<ThemeProvider theme={theme}>
<ApolloHooksProvider client={client}>
<ApolloProvider client={client}>
<Suspense fallback={<Loading />}>
<ErrorBoundary>
<App />
</ErrorBoundary>
</Suspense>
</ApolloProvider>
</ApolloHooksProvider>
</ThemeProvider>
</React.Fragment>
);
}
}
================================================
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 (
<Frame
src={`${getAPIUrl()}/coverage/lcov-report/index.html`}
frameBorder="0"
/>
);
}
================================================
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 (
<Container>
<Loader>
<svg
id="icon-stop-circle"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="aliceblue"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<rect className="stopping" x="9" y="9" width="6" height="6" />
</svg>
</Loader>
<Message>
Oops, Something went wrong. Check the terminal for exact error
message!
</Message>
</Container>
);
}
}
================================================
FILE: ui/hooks/use-keys.ts
================================================
import { useEffect, useState } from "react";
export function hasKeys(expectedKeys: string[], pressedKeys: Map<String, boolean> ) {
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(<Container />, document.getElementById("root"));
if ((module as any).hot) {
(module as any).hot.accept("./container", () => {
const NextApp = require("./container").default;
ReactDOM.render(<Container />, 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 (
<Container>
<Loader>
<svg
id="icon-crop-button"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 459 459"
>
<path d="M0 51v102h51V51h102V0H51C23 0 0 23 0 51zm51 255H0v102c0 28 23 51 51 51h102v-51H51V306zm357 102H306v51h102c28 0 51-23 51-51V306h-51v102zm0-408H306v51h102v102h51V51c0-28-23-51-51-51z" />
</svg>
</Loader>
<Message>Getting things ready for you</Message>
</Container>
);
}
================================================
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<any>`
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<HTMLInputElement>(null);
useEffect(() => {
if (searchBoxRef && searchBoxRef.current) {
searchBoxRef.current.focus();
}
}, [show]);
if (!show) return null;
return (
<React.Fragment>
<Drop onClick={onClose} />
<Container bg="dark">
<SearchBox
ref={searchBoxRef}
value={query}
placeholder="Start searching…"
onChange={(event: any) => {
setQuery(event.target.value);
}}
/>
<ResultContainer>
{onlyFiles
.filter(file =>
file.path.toLowerCase().includes(query.toLowerCase())
)
.map((file: any, index: number) => (
<ItemContainer
key={index}
onClick={() => {
onItemClick(file.path);
}}
>
{file.path.toLowerCase().replace(projectRoot.toLowerCase(), "")}
</ItemContainer>
))}
</ResultContainer>
</Container>
</React.Fragment>
);
}
================================================
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 (
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="18px"
height="12px"
viewBox="0 0 24 30"
>
<rect x="0" y="0" width="4" height="15" fill="#fcd101">
<animate
attributeName="opacity"
attributeType="XML"
values="1; .2; 1"
begin="0s"
dur="0.6s"
repeatCount="indefinite"
/>
</rect>
<rect x="7" y="0" width="4" height="15" fill="#DFBD39">
<animate
attributeName="opacity"
attributeType="XML"
values="1; .2; 1"
begin="0.2s"
dur="0.6s"
repeatCount="indefinite"
/>
</rect>
<rect x="14" y="0" width="4" height="15" fill="#fcd101">
<animate
attributeName="opacity"
attributeType="XML"
values="1; .2; 1"
begin="0.4s"
dur="0.6s"
repeatCount="indefinite"
/>
</rect>
</svg>
);
}
================================================
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<any>`
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 (
<Container
style={{
...style,
width: "90%",
marginLeft: `${(item.hierarchy + 1) * 15}px`,
}}
>
<Content
hierarchy={item.hierarchy}
passing={item.passing}
failed={item.haveFailure}
selected={selectedFile === item.path}
onClick={handleClick}
>
<Chevron size={11} />
{!item.isExecuting && <Icon size={11} />}
{item.isExecuting && (
<ExecutionWrapper>
<ExecutionIndicator />
</ExecutionWrapper>
)}
<Label>{item.name}</Label>
</Content>
</Container>
);
}
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<any>`
${space};
${color};
height: 100vh;
`;
const ActionsPanel = styled.div<any>`
${space}
display: flex;
justify-content: space-between;
`;
const RightActionPanel = styled.div`
display: flex;
`;
const FileHeader = styled.div<any>`
${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<any>(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 (
<Container p={4} bg="veryDark" color="text">
<Logo />
<ActionsPanel mb={4}>
<Tooltip title="Run all tests" position="bottom" size="small">
<Button
icon={isRunning ? <StopCircle size={15} /> : <Play size={15} />}
size="sm"
onClick={() => {
if (isRunning) {
onStop();
} else {
run();
}
}}
>
{isRunning ? "Stop" : "Run tests"}
</Button>
</Tooltip>
<RightActionPanel>
<Tooltip title="Toggle watch mode" position="bottom" size="small">
<Button
icon={<Eye size={14} />}
minimal
onClick={() => {
if (runnerStatus) {
handleSetWatchModel(!runnerStatus.watching);
}
}}
>
{runnerStatus && runnerStatus.watching
? "Stop Watching"
: "Watch"}
</Button>
</Tooltip>
<Tooltip title="Collect coverage" position="bottom" size="small">
<Button
minimal={!shouldCollectCoverage}
onClick={() => {
setCollectCoverage({
variables: {
collect: !shouldCollectCoverage
}
});
refetchCoverageFlag();
}}
>
<FileText size={14} />
</Button>
</Tooltip>
<Tooltip title="Search test files" position="bottom" size="small">
<Button
minimal
onClick={() => {
onSearchOpen();
}}
>
<Search size={14} />
</Button>
</Tooltip>
</RightActionPanel>
</ActionsPanel>
<Summary summary={summary} />
<FileHeader mt={4} mb={3}>
<FilesHeader>Tests</FilesHeader>
<RightFilesAction>
{summary && summary.failedTests && summary.failedTests.length > 0 && (
<Tooltip
title="Show only failed tests"
position="top"
size="small"
>
<Button
size="sm"
minimal={!showFailedTests}
onClick={() => {
setShowFailedTests(!showFailedTests);
}}
>
<ZapOff size={10} />
</Button>
</Tooltip>
)}
{!showFailedTests && (
<Tooltip title="Collapse All Tests" position="top" size="small">
<Button
size="sm"
minimal
onClick={onCollapseAll}
>
<ChevronRight size={10} />
</Button>
</Tooltip>
)}
{!showFailedTests && (
<Tooltip title="Expand All Tests" position="top" size="small">
<Button
size="sm"
minimal
onClick={onExpandAll}
>
<ChevronDown size={10} />
</Button>
</Tooltip>
)}
{summary && summary.haveCoverageReport && (
<Tooltip
title="Show coverage report"
position="top"
size="small"
>
<Button
size="sm"
minimal={!showCoverage}
onClick={() => {
onShowCoverage();
}}
>
<Layers size={10} />
</Button>
</Tooltip>
)}
<Tooltip title="Refresh files" position="top" size="small">
<Button
size="sm"
minimal
onClick={() => {
onRefreshFiles();
}}
>
<RefreshCw size={10} />
</Button>
</Tooltip>
</RightFilesAction>
</FileHeader>
<Tree
results={files}
selectedFile={selectedFile}
onFileSelection={handleFileSelection}
onToggle={handleFileToggle}
/>
</Container>
);
}
================================================
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 (
<Container>
<img width={200} src={logo} />
</Container>
);
}
================================================
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<any>`
${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<any>`
font-size: 20px;
color: ${props => (props.failed ? "#FF4F56" : "#19E28D")};
`;
const CoverageLabel = styled.div`
font-size: 10px;
color: #dcdbdb;
`;
const CoverageValue = styled.div<any>`
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 (
<Container mt={3} mb={3}>
<Row>
<Cell>
<Value>
<animated.span>
{(passedSuitesProps as any).number.interpolate((value: any) =>
value.toFixed()
)}
</animated.span>
</Value>
<Label>
<CheckCircle size={11} /> Passing suites
</Label>
</Cell>
<Cell>
<Value failed>
<animated.span>
{(failedSuitesProps as any).number.interpolate((value: any) =>
value.toFixed()
)}
</animated.span>
</Value>
<Label>
<ZapOff size={11} /> Failing suites
</Label>
</Cell>
</Row>
<Row>
<Cell>
<Value>
<animated.span>
{(passedTestProps as any).number.interpolate((value: any) =>
value.toFixed()
)}
</animated.span>
</Value>
<Label>
<CheckCircle size={11} /> Passing tests
</Label>
</Cell>
<Cell>
<Value failed>
<animated.span>
{(failedTestProps as any).number.interpolate((value: any) =>
value.toFixed()
)}
</animated.span>
</Value>
<Label>
<ZapOff size={11} /> Failing tests
</Label>
</Cell>
</Row>
{!!haveCoverage && (
<Coverage>
<Row>
<Cell>
<CoverageValue>
{summary && summary.coverage && summary.coverage.statement}%
</CoverageValue>
<CoverageLabel>
<Layers size={9} /> Stmts
</CoverageLabel>
</Cell>
<Cell>
<CoverageValue>
{summary && summary.coverage && summary.coverage.branch}%
</CoverageValue>
<CoverageLabel>
<Layers size={9} /> Branch
</CoverageLabel>
</Cell>
<Cell>
<CoverageValue>
{summary && summary.coverage && summary.coverage.function}%
</CoverageValue>
<CoverageLabel>
<Layers size={9} /> Funcs
</CoverageLabel>
</Cell>
<Cell>
<CoverageValue>
{summary && summary.coverage && summary.coverage.line}%
</CoverageValue>
<CoverageLabel>
<Layers size={9} /> Lines
</CoverageLabel>
</Cell>
</Row>
</Coverage>
)}
</Container>
);
}
================================================
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 (
<FileTreeContainer>
<AutoResizer>
{({ height, width }: any) => {
return (
<List
height={height - 90}
itemCount={results.length}
itemSize={20}
width={width}
>
{({ index, style }: any) => (
<FileItem
style={style}
key={results[index].path}
item={results[index]}
selectedFile={selectedFile}
setSelectedFile={onFileSelection}
onToggle={onToggle}
/>
)}
</List>
);
}}
</AutoResizer>
</FileTreeContainer>
);
}
================================================
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 = <AlertCircle size={11} color="#FDC055" />;
break;
case "error":
icon = <XCircle size={11} color="#ff4f56" />;
break;
case "log":
icon = <MessageSquare size={11} color="#19E28D" />;
break;
}
return <IconWrapper>{icon}</IconWrapper>;
}
interface Props {
consoleLogs: ConsoleLog[];
}
export default function ConsolePanel({ consoleLogs }: Props) {
return (
<Container>
<Header>Console logs from the file</Header>
<Logs>
{consoleLogs.map((log, index) => {
let result = log.message;
try {
result = eval("(" + log.message + ")");
} catch (e) {
console.log(e);
}
if (typeof result === "string") {
return (
<Content key={index}>
{getIcon(log.type)}
{cleanAnsiCodes(result)}
</Content>
);
}
return (
<Content>
{getIcon(log.type)}
<ObjectInspector
data={result}
theme={{
...chromeDark,
...{
TREENODE_FONT_SIZE: "12px",
BASE_BACKGROUND_COLOR: "#404148",
ARROW_FONT_SIZE: 10
}
}}
/>
</Content>
);
})}
</Logs>
</Container>
);
}
================================================
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, """)
.replace(/'/g, "'");
}
export default function ErrorPanel({ failureMessage }: Props) {
if (!failureMessage || failureMessage.trim() === "") {
return null;
}
return (
<Container>
<pre
dangerouslySetInnerHTML={{
__html: convert.toHtml(escapeHtml(failureMessage))
}}
/>
</Container>
);
}
================================================
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<any>`
${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 (
<Container p={5} bg="dark" color="text">
<FileSummary
projectRoot={projectRoot}
suiteCount={suiteCount}
testCount={testCount}
todoCount={todoCount}
passingTests={result && result.numPassingTests}
failingTests={result && result.numFailingTests}
path={selectedFilePath}
isRunning={isRunning}
isUpdating={isUpdating}
isLoadingResult={loading}
onRun={() => {
runFile();
}}
onStop={onStop}
onSnapshotUpdate={() => {
updateSnapshot();
}}
/>
<Content dim={isUpdating}>
{result && result.testResults && result.testResults.length === 0 && (
<ErrorPanel failureMessage={result && result.failureMessage} />
)}
{result && result.consoleLogs && result.consoleLogs.length > 0 && (
<ConsolePanel consoleLogs={result.consoleLogs || []} />
)}
{fileItemResult && (
<TestItemsContainer>
{roots.map(item => {
const tree = transform(
item as any,
fileItemResult.items as any,
0
) as any;
return <Test key={item.id} item={tree} result={result} />;
})}
</TestItemsContainer>
)}
</Content>
</Container>
);
}
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<any>`
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<any>`
${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 (
<Container p={4} bg="slightDark">
{( isUpdating || isLoadingResult) && <ContainerBG />}
<RightContainer>
<FilePath fontSize={15} mb={3}>
{path.replace(projectRoot, "")}
</FilePath>
<InfoContainer>
<Info color="primary">
<Folder size={14} /> <InfoLabel>{suiteCount} Suites</InfoLabel>
</Info>
<Info color="primary">
<Code size={14} /> <InfoLabel>{testCount} Tests</InfoLabel>
</Info>
<Info color="success">
<CheckCircle size={14} />{" "}
<InfoLabel>{passingTests} Passing tests</InfoLabel>
</Info>
<Info color="danger">
<ZapOff size={14} />{" "}
<InfoLabel>{failingTests} Failing tests</InfoLabel>
</Info>
</InfoContainer>
</RightContainer>
<ActionPanel>
{isLoadingResult && <LoadingResult>Loading test results</LoadingResult>}
<Tooltip title="Run file" position="bottom" size="small">
<Button
icon={<Icon size={14} />}
minimal
onClick={() => {
if (isRunning) {
onStop();
} else {
onRun();
}
}}
>
{isRunning ? "Stop" : "Run"}
</Button>
</Tooltip>
<Tooltip title="Open in editor" size="small" position="bottom">
<Button
icon={<Code size={14} />}
minimal
onClick={() => {
openInEditor();
}}
/>
</Tooltip>
<Tooltip
title="Update all snapshots for this file"
position="bottom"
size="small"
>
<Button
minimal
icon={<Camera size={14} />}
onClick={() => {
onSnapshotUpdate();
}}
>
Update Snapshot
</Button>
</Tooltip>
<Tooltip title="Open snapshot in editor" size="small" position="bottom">
<Button
icon={<Eye size={14} />}
minimal
onClick={() => {
openSnapshotInEditor();
}}
/>
</Tooltip>
</ActionPanel>
</Container>
);
}
================================================
FILE: ui/test-file/summary/open-in-editor.gql
================================================
mutation OpenInEditor($path: String!) {
openInEditor(path: $path)
}
================================================
FILE: ui/test-file/summary/open-snap-in-editor.gql
================================================
mutation OpenSnapInEditor($path: String!) {
openSnapInEditor(path: $path)
}
================================================
FILE: ui/test-file/test-indicator.tsx
================================================
import React from "react";
import {
CheckCircle,
Circle,
Package,
XCircle,
Zap,
Edit2
} from "react-feather";
interface Props {
status: string | null | undefined;
describe: boolean;
todo: boolean;
}
export default function TestIndicator({ status, describe, todo }: Props) {
let Icon = describe ? Package : Zap;
let color = "#AC61FF";
if (todo) {
return <Edit2 size={14} color="#AC61FF" />;
}
if (!describe) {
if (status === "passed") {
Icon = CheckCircle;
} else if (status === "todo") {
Icon = Circle;
} else if (status === "failed") {
Icon = XCircle;
}
}
if (status === "passed") {
color = "#50E3C2";
} else if (status === "failed") {
color = "#FF4954";
}
return <Icon size={14} color={color} />;
}
================================================
FILE: ui/test-file/test-item.tsx
================================================
import React, { Fragment } from "react";
import styled from "styled-components";
import { TestFileItem } from "./transformer";
import { TestFileResult } from "../../server/api/workspace/test-result/file-result";
import TestIndicator from "./test-indicator";
import { color, space } from "styled-system";
import * as Convert from "ansi-to-html";
import OPEN_FAILURE from "./open-failure.gql";
import { useMutation } from "react-apollo-hooks";
const convert = new Convert({
colors: {
1: "#FF4F56",
2: "#19E28D"
}
});
function getResults(item: TestFileItem, testResult: TestFileResult) {
if (!testResult || !testResult.testResults) {
return null;
}
return testResult.testResults.find(result => result.title === item.name);
}
const Container = styled.div`
${color};
${space};
padding-left: 25px;
`;
const Label = styled.div`
display: flex;
align-items: center;
font-weight: 600;
font-size: 13px;
span {
margin-left: 5px;
}
`;
const Content = styled.div<any>`
padding: 5px;
display: flex;
flex-direction: column;
background-color: #262529;
border-radius: 4px;
margin-bottom: 10px;
border: 1px solid ${props => (props.only ? "#9d8301" : "#333437")};
`;
const FailureMessage = styled.div`
padding-left: 20px;
pre {
overflow: auto;
}
`;
const Duration = styled.span`
font-weight: 400;
font-size: 12px;
color: #fcd101;
`;
function escapeHtml(unsafe: string) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
interface Props {
item: TestFileItem;
result: TestFileResult | null;
}
export default function Test({
item: { name, only, children },
item,
result
}: Props) {
const testResult = getResults(item, result as any);
const isDurationAvailable = testResult && testResult.duration !== undefined;
const haveFailure = testResult && testResult.failureMessages.length > 0;
const allChildrenPassing = (children || []).every(child => {
if (child.type === "it") {
const childResult = getResults(child, result as any);
return childResult && childResult.status === "passed";
}
return true;
});
if (children && children.length > 0) {
}
const openFailure = useMutation(OPEN_FAILURE, {
variables: {
failure: testResult && testResult.failureMessages ? testResult.failureMessages[0] : ''
}
});
return (
<Container>
<Content only={only} onClick={ () => openFailure()}>
<Label>
<TestIndicator
status={
item.type === "describe" && allChildrenPassing
? "passed"
: testResult && testResult.status
}
describe={item.type === "describe"}
todo={item.type === "todo"}
/>
<span>{name}</span>
{isDurationAvailable && (
<Duration>{testResult && testResult.duration} ms</Duration>
)}
</Label>
{testResult && haveFailure && (
<FailureMessage>
<pre
dangerouslySetInnerHTML={{
__html: convert.toHtml(
escapeHtml(testResult.failureMessages.join(","))
)
}}
/>
</FailureMessage>
)}
</Content>
{children &&
children.map(child => (
<Test key={child.id} item={child} result={result} />
))}
</Container>
);
}
================================================
FILE: ui/test-file/transformer.ts
================================================
import { TestItem } from "../../server/api/workspace/test-item";
export interface TestFileItem extends TestItem {
children?: TestFileItem[];
index: number;
}
export function transform(
item: TestFileItem,
items: TestItem[],
index: number = 0,
tree?: TestFileItem
) {
if (!item) {
return {};
}
const nextChildren = getChildren(item.id, items);
if (!tree) {
tree = {
id: item.id,
type: item.type,
name: item.name,
parent: item.parent,
only: item.only,
children: nextChildren,
index: index + 1
} as any;
}
item.children = nextChildren as any;
item.children &&
item.children.forEach(item => {
transform(item, items, index + 1, tree);
});
return tree;
}
function getChildren(id: string, items: TestItem[]) {
return items.filter(item => item.parent === id);
}
================================================
FILE: ui/test-file/update-snapshot.gql
================================================
mutation UpdateSnapshot($path: String!) {
updateSnapshot(path: $path)
}
================================================
FILE: ui/test-file/use-subscription.tsx
================================================
import React, { useState, useEffect } from "react";
import { DocumentNode } from "graphql";
import { useApolloClient } from "react-apollo-hooks";
export default function useSubscription(
query: DocumentNode,
subscriptionQuery: DocumentNode,
variables: any,
queryResultMapper: (result: any) => any,
subResultMapper: (result: any) => any,
name: string = ""
) {
const client = useApolloClient();
const [result, setResult] = useState<any>({
data: {},
loading: false,
error: null
});
let subscription: any;
useEffect(
() => {
if (client) {
setResult({
...result,
loading: true
});
client
.query({
query,
variables,
fetchPolicy: "network-only"
})
.then(({ data, errors, loading }) => {
console.log(name, data);
setResult({
data: queryResultMapper(data),
error: errors,
loading
});
});
}
},
variables.path ? [variables.path] : []
);
useEffect(
() => {
if (client) {
console.log("Subbed to", name);
subscription = client
.subscribe({
query: subscriptionQuery,
variables,
fetchPolicy: "network-only"
})
.subscribe({
error: (error: any) => {
setResult({ loading: false, data: result.data, error });
},
next: (nextResult: any) => {
console.log("Sub Result", name, nextResult.data);
const newResult = {
data: subResultMapper(nextResult.data),
error: undefined,
loading: false
};
setResult(newResult);
}
});
}
},
variables.path ? [variables.path] : []
);
useEffect(
() => {
return () => {
subscription && subscription.unsubscribe();
};
},
variables.path ? [variables.path] : []
);
return result;
}
================================================
FILE: ui/theme.ts
================================================
export default {
colors: {
veryDark: "#262529",
dark: "#242326",
slightDark: "#404148",
text: "#F5F5F5",
primary: "#FDC055",
danger: "#ff4f56",
success: "#19E28D"
},
space: [0, 2, 4, 8, 16, 32, 64, 128, 256, 512]
};
================================================
FILE: ui/typings.d.ts
================================================
declare module "apollo-link-ws";
declare module "apollo-utilities";
declare module "apollo-link";
declare module "apollo-client-preset";
declare module "*.gql" {
const content: any;
export default content;
}
declare module "resolve-pkg";
declare module "react-virtualized-auto-sizer";
declare module "react-window";
declare module "minimist";
declare module "react-tippy";
declare module "ansi-to-html";
declare module "*.png";
declare module "react-inspector";
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
SYMBOL INDEX (168 symbols across 54 files)
FILE: integration/projects/basic/app.js
class App (line 3) | class App extends Component {
method render (line 4) | render() {
FILE: server/api/app/app.ts
class App (line 4) | class App {
FILE: server/api/app/resolver.ts
class AppResolver (line 9) | class AppResolver {
method constructor (line 13) | constructor() {
method app (line 19) | app() {
method setSelectedFile (line 24) | setSelectedFile(@Arg("path", { nullable: true }) path: string) {
method openInEditor (line 41) | openInEditor(@Arg("path") path: string) {
method openSnapInEditor (line 50) | openSnapInEditor(@Arg("path") path: string) {
method openFailure (line 63) | openFailure(@Arg("failure") failure: string) {
FILE: server/api/index.ts
function getSchema (line 7) | async function getSchema() {
FILE: server/api/runner/resolver.ts
class RunnerResolver (line 21) | class RunnerResolver {
method constructor (line 29) | constructor() {
method runnerStatus (line 37) | runnerStatus() {
method shouldCollectCoverage (line 46) | shouldCollectCoverage() {
method runnerStatusChange (line 58) | runnerStatusChange(@Root() event: RunnerEvent) {
method runFile (line 72) | runFile(@Arg("path") path: string) {
method run (line 92) | run() {
method stop (line 99) | stop() {
method updateSnapshot (line 104) | updateSnapshot(@Arg("path") path: string) {
method toggleWatch (line 110) | toggleWatch(@Arg("watch") watch: boolean) {
method setCollectCoverage (line 120) | setCollectCoverage(@Arg("collect") collect: boolean) {
FILE: server/api/runner/status.ts
class RunnerStatus (line 4) | class RunnerStatus {
FILE: server/api/runner/type.ts
class Runner (line 4) | class Runner {
FILE: server/api/workspace/coverage.ts
class CoverageSummary (line 4) | class CoverageSummary {
FILE: server/api/workspace/resolver.ts
class WorkspaceResolver (line 32) | class WorkspaceResolver {
method constructor (line 37) | constructor() {
method workspace (line 104) | workspace() {
method file (line 121) | async file(@Arg("path") path: string) {
method result (line 128) | result(@Arg("path") path: string) {
method fileChange (line 136) | async fileChange(@Root() event: FileChangeEvent, @Arg("path") path: st...
method changeToResult (line 152) | async changeToResult(
method changeToSummary (line 179) | async changeToSummary(@Root() event: SummaryEvent): Promise<Summary> {
method summary (line 201) | summary() {
FILE: server/api/workspace/summary.ts
class Summary (line 5) | class Summary {
FILE: server/api/workspace/test-file.ts
class TestFile (line 5) | class TestFile {
FILE: server/api/workspace/test-item.ts
type TestItemType (line 3) | type TestItemType = "describe" | "it" | "todo";
class TestItem (line 6) | class TestItem {
FILE: server/api/workspace/test-result/console-log.ts
class ConsoleLog (line 4) | class ConsoleLog {
FILE: server/api/workspace/test-result/file-result.ts
class TestFileResult (line 6) | class TestFileResult {
FILE: server/api/workspace/test-result/test-item-result.ts
class TestItemResult (line 4) | class TestItemResult {
FILE: server/api/workspace/tree.ts
class Item (line 4) | class Item {
FILE: server/api/workspace/workspace.ts
class Workspace (line 5) | class Workspace {
FILE: server/index.ts
function main (line 32) | async function main() {
FILE: server/logger.ts
function debugLog (line 3) | function debugLog(tag: string, ...args: any) {
function executeAndLog (line 12) | function executeAndLog(
function createLogger (line 25) | function createLogger(tag: string) {
FILE: server/services/ast/inspector.ts
function inspect (line 7) | async function inspect(path: string): Promise<TestItem[]> {
function getTemplateLiteralName (line 41) | function getTemplateLiteralName(path: any) {
function findItems (line 60) | function findItems(path: any, result: TestItem[], parentId?: any) {
FILE: server/services/ast/parser.ts
function parse (line 4) | function parse(path: string, code: string) {
FILE: server/services/config-resolver.ts
class ConfigResolver (line 13) | class ConfigResolver {
method getConfig (line 14) | public getConfig(projectRoot: string): MajesticConfig {
method getJestScriptPath (line 61) | private getJestScriptPath(projectRoot: string) {
method getJestScriptForCreateReactApp (line 76) | private getJestScriptForCreateReactApp(projectRoot: string) {
method getPackageJson (line 83) | private getPackageJson(rootPath: string) {
method getConfigFromPackageJson (line 89) | private getConfigFromPackageJson(projectRoot: string) {
method isBootstrappedWithCreateReactApp (line 97) | private isBootstrappedWithCreateReactApp(rootPath: string): boolean {
method hasExecutable (line 108) | private hasExecutable(rootPath: string, executablePath: string): boole...
FILE: server/services/file-watcher/index.ts
type FileChangeEvent (line 11) | interface FileChangeEvent {
class FileWatcher (line 18) | class FileWatcher {
method watch (line 21) | watch(filePath: string) {
FILE: server/services/jest-manager/index.ts
type RunnerEvent (line 17) | interface RunnerEvent {
class JestManager (line 24) | class JestManager {
method constructor (line 29) | constructor(project: Project, config: MajesticConfig) {
method run (line 34) | run(watch: boolean, collectCoverage: boolean) {
method runSingleFile (line 47) | runSingleFile(path: string, watch: boolean, collectCoverage: boolean) {
method updateSnapshotToFile (line 63) | updateSnapshotToFile(path: string) {
method switchToAnotherFile (line 77) | switchToAnotherFile(path: string) {
method executeJest (line 98) | executeJest(
method getReporterPath (line 152) | getReporterPath() {
method getPatchFilePath (line 156) | getPatchFilePath() {
method getPatternForPath (line 160) | getPatternForPath(path: string) {
method reportStart (line 168) | reportStart() {
method stop (line 177) | stop() {
method reportStop (line 190) | reportStop() {
method executeInSequence (line 199) | async executeInSequence(
method setTimeoutPromisify (line 210) | setTimeoutPromisify(fn: () => void, delay: number) {
method getWatchFlag (line 219) | getWatchFlag() {
method isInGitRepository (line 223) | isInGitRepository() {
FILE: server/services/jest-manager/scripts/reporter.js
function send (line 3) | function send(type, body) {
class MyCustomReporter (line 11) | class MyCustomReporter {
method constructor (line 12) | constructor(globalConfig, options) {
method onTestStart (line 17) | onTestStart(test) {
method onTestResult (line 23) | onTestResult(test, testResult, aggregatedResult) {
method onRunStart (line 51) | onRunStart(results) {}
method onRunComplete (line 53) | onRunComplete(contexts, results) {
FILE: server/services/project.ts
class Project (line 8) | class Project {
method constructor (line 11) | constructor(root: string) {
method getFilesList (line 15) | getFilesList(config: MajesticConfig) {
FILE: server/services/result-handler-api.ts
type ResultEvent (line 16) | interface ResultEvent {
type SummaryEvent (line 21) | interface SummaryEvent {
function handlerApi (line 33) | function handlerApi(expressApp: Application) {
FILE: server/services/results.ts
type TestFileStatus (line 12) | type TestFileStatus = "IDLE" | "EXECUTING";
type CoverageSummary (line 13) | interface CoverageSummary {
class Results (line 19) | class Results {
method constructor (line 51) | constructor(projectRoot: string) {
method setTestStart (line 64) | public setTestStart(path: string) {
method setTestReport (line 74) | public setTestReport(path: string, report: any) {
method getResult (line 85) | public getResult(path: string): TestFileResult | null {
method setSummary (line 89) | public setSummary(
method markExecutingAsStopped (line 103) | public markExecutingAsStopped() {
method getSummary (line 116) | public getSummary() {
method getFailedTests (line 120) | public getFailedTests() {
method getPassedTests (line 128) | public getPassedTests() {
method getExecutingTests (line 136) | public getExecutingTests() {
method mapCoverage (line 144) | public mapCoverage(data: any) {
method checkIfCoverageReportExists (line 174) | public checkIfCoverageReportExists() {
method getCoverage (line 179) | public getCoverage() {
method doesHaveCoverageReport (line 183) | public doesHaveCoverageReport() {
method getCoverageReportPath (line 187) | public getCoverageReportPath(config: MajesticConfig) {
FILE: server/services/types.ts
type DirectoryItem (line 1) | interface DirectoryItem {
type TreeMap (line 8) | interface TreeMap {
type MajesticConfig (line 17) | interface MajesticConfig {
FILE: server/static-files.ts
function initializeStaticRoutes (line 5) | function initializeStaticRoutes(express: exp.Application, root: string) {
FILE: ui/apollo-client.ts
constant WS_URL (line 8) | let WS_URL = "ws://localhost:4000";
constant HTTP_URL (line 9) | let HTTP_URL = "http://localhost:4000";
function getAPIUrl (line 16) | function getAPIUrl() {
FILE: ui/app.tsx
type AppResult (line 36) | interface AppResult {
type WorkspaceResult (line 40) | interface WorkspaceResult {
function App (line 44) | function App() {
FILE: ui/components/button.tsx
function Button (line 32) | function Button(props: any) {
FILE: ui/container.tsx
class Container (line 19) | class Container extends Component {
method render (line 20) | render() {
FILE: ui/coverage-panel/index.tsx
function CoveragePanel (line 10) | function CoveragePanel() {
FILE: ui/error.tsx
class ErrorBoundary (line 54) | class ErrorBoundary extends Component {
method componentDidCatch (line 59) | componentDidCatch() {
method render (line 65) | render() {
FILE: ui/hooks/use-keys.ts
function hasKeys (line 3) | function hasKeys(expectedKeys: string[], pressedKeys: Map<String, boolea...
function useKeys (line 7) | function useKeys() {
FILE: ui/loading.tsx
function Loading (line 65) | function Loading() {
FILE: ui/search/index.tsx
type Props (line 70) | interface Props {
function Search (line 78) | function Search({
FILE: ui/sidebar/execution-indicator.tsx
function ExecutionIndicator (line 3) | function ExecutionIndicator() {
FILE: ui/sidebar/file-item.tsx
type Props (line 53) | interface Props {
function FileItem (line 61) | function FileItem({
FILE: ui/sidebar/index.tsx
type Props (line 65) | interface Props {
function TestExplorer (line 78) | function TestExplorer ({
FILE: ui/sidebar/logo.tsx
function Logo (line 11) | function Logo() {
FILE: ui/sidebar/summary/index.tsx
type Props (line 47) | interface Props {
function SummaryPanel (line 51) | function SummaryPanel({ summary }: Props) {
FILE: ui/sidebar/transformer.ts
type TreeNode (line 3) | interface TreeNode extends Item {
function transform (line 13) | function transform(
function haveFailedChildren (line 77) | function haveFailedChildren(path: string, results: TreeNode[]) {
function sortAsc (line 87) | function sortAsc(a: Item, b: Item){
function getChildren (line 91) | function getChildren(path: string, files: Item[]) {
FILE: ui/sidebar/tree.tsx
type Props (line 14) | interface Props {
function Tree (line 21) | function Tree({
FILE: ui/test-file/console-panel/index.tsx
function getIcon (line 45) | function getIcon(type: String) {
type Props (line 62) | interface Props {
function ConsolePanel (line 67) | function ConsolePanel({ consoleLogs }: Props) {
FILE: ui/test-file/error-panel/index.tsx
type Props (line 19) | interface Props {
function escapeHtml (line 23) | function escapeHtml(unsafe: string) {
function ErrorPanel (line 32) | function ErrorPanel({ failureMessage }: Props) {
FILE: ui/test-file/index.tsx
type Props (line 41) | interface Props {
function TestFile (line 48) | function TestFile({ selectedFilePath, isRunning, projectRoot, onStop }: ...
FILE: ui/test-file/summary/index.tsx
type Props (line 104) | interface Props {
function FileSummary (line 121) | function FileSummary({
FILE: ui/test-file/test-indicator.tsx
type Props (line 11) | interface Props {
function TestIndicator (line 17) | function TestIndicator({ status, describe, todo }: Props) {
FILE: ui/test-file/test-item.tsx
function getResults (line 18) | function getResults(item: TestFileItem, testResult: TestFileResult) {
function escapeHtml (line 67) | function escapeHtml(unsafe: string) {
type Props (line 76) | interface Props {
function Test (line 81) | function Test({
FILE: ui/test-file/transformer.ts
type TestFileItem (line 3) | interface TestFileItem extends TestItem {
function transform (line 8) | function transform(
function getChildren (line 38) | function getChildren(id: string, items: TestItem[]) {
FILE: ui/test-file/use-subscription.tsx
function useSubscription (line 5) | function useSubscription(
Condensed preview — 120 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (158K chars).
[
{
"path": ".all-contributorsrc",
"chars": 5273,
"preview": "{\n \"files\": [\n \"README.md\"\n ],\n \"imageSize\": 100,\n \"commit\": false,\n \"contributors\": [\n {\n \"login\": \"dun"
},
{
"path": ".babelrc",
"chars": 427,
"preview": "{\n \"presets\": [\n \"@babel/preset-react\",\n [\n \"@babel/preset-typescript\",\n {\n \"isTSX\": true,\n "
},
{
"path": ".github/issue_template.md",
"chars": 502,
"preview": "## Is this a bug report or a feature request?\n\nPlease specify whether this is a feature request or a bug.\n\n## Version In"
},
{
"path": ".github/stale.yml",
"chars": 282,
"preview": "daysUntilStale: 30\ndaysUntilClose: 7\nonlyLabels: [🦄 Need more info]\nmarkComment: >\n This issue has been automatically m"
},
{
"path": ".github/workflows/nodejs.yml",
"chars": 487,
"preview": "name: Node CI\n\non: [push]\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n strategy:\n matrix:\n node-version: "
},
{
"path": ".gitignore",
"chars": 17,
"preview": "node_modules\ndist"
},
{
"path": ".prettierrc",
"chars": 52,
"preview": "{\n \"singleQuote\": true,\n \"trailingComma\": \"all\"\n}\n"
},
{
"path": ".vscode/launch.json",
"chars": 604,
"preview": "{\n // Use IntelliSense to learn about possible attributes.\n // Hover to view descriptions of existing attributes.\n //"
},
{
"path": "CONTRIBUTING.MD",
"chars": 1541,
"preview": "### Preparing Majestic\n\n- Clone this repository\n- Install dependencies with `yarn install`\n\n### Running Majestic\n\nMajest"
},
{
"path": "LICENSE",
"chars": 1079,
"preview": "MIT License\n\nCopyright (c) 2018 Raathigeshan Kugarajan\n\nPermission is hereby granted, free of charge, to any person obta"
},
{
"path": "README.md",
"chars": 10788,
"preview": "<div align=\"center\">\n<img src=\"./image.png\" />\n<br />\n<br />\n<a href=\"https://github.com/Raathigesh/majestic/actions\">\n"
},
{
"path": "Troubleshooting.md",
"chars": 874,
"preview": "#### Custom react-scripts\n\nIf you're using a custom [react-scripts](https://www.npmjs.com/package/react-scripts) in your"
},
{
"path": "integration/cypress/fixtures/example.json",
"chars": 154,
"preview": "{\n \"name\": \"Using fixtures to represent data\",\n \"email\": \"hello@cypress.io\",\n \"body\": \"Fixtures are a great way to mo"
},
{
"path": "integration/cypress/integration/basic/basic-functionality.js",
"chars": 1287,
"preview": "/// <reference types=\"Cypress\" />\n\ncontext('basic', () => {\n beforeEach(() => {\n cy.visit('http://localhost:9000', {"
},
{
"path": "integration/cypress/plugins/index.js",
"chars": 645,
"preview": "// ***********************************************************\n// This example plugins/index.js can be used to load plug"
},
{
"path": "integration/cypress/support/commands.js",
"chars": 888,
"preview": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom"
},
{
"path": "integration/cypress/support/index.js",
"chars": 671,
"preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
},
{
"path": "integration/cypress.json",
"chars": 28,
"preview": "{\n \"projectId\": \"q19erz\"\n}\n"
},
{
"path": "integration/kill.js",
"chars": 207,
"preview": "const fkill = require('fkill');\n\nfkill(':9000', {\n force: true,\n tree: true,\n})\n .then(() => {\n console.log('Kille"
},
{
"path": "integration/package.json",
"chars": 865,
"preview": "{\n \"name\": \"integration\",\n \"version\": \"1.0.0\",\n \"main\": \"index.js\",\n \"license\": \"MIT\",\n \"scripts\": {\n \"prepare-p"
},
{
"path": "integration/projects/basic/__snapshots__/test-snapshot-failure.spec.js.snap",
"chars": 129,
"preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`test Snapsh0t test 1`] = `\n<div\n className=\"App\"\n>\n Hello world\n<"
},
{
"path": "integration/projects/basic/app.js",
"chars": 168,
"preview": "import React, { Component } from 'react';\n\nclass App extends Component {\n render() {\n return <div className=\"App\">He"
},
{
"path": "integration/projects/basic/babel.config.js",
"chars": 181,
"preview": "module.exports = {\n presets: [\n [\n '@babel/preset-env',\n {\n targets: {\n node: 'current',\n "
},
{
"path": "integration/projects/basic/package.json",
"chars": 425,
"preview": "{\n \"name\": \"simple-jest\",\n \"version\": \"1.0.0\",\n \"main\": \"index.js\",\n \"license\": \"MIT\",\n \"scripts\": {\n \"test\": \"j"
},
{
"path": "integration/projects/basic/test-all-good.spec.js",
"chars": 386,
"preview": "describe('test', () => {\n it('should add third', () => {\n expect(5).toBe(5);\n });\n\n it('should add 1', () => {\n "
},
{
"path": "integration/projects/basic/test-few-failure.spec.js",
"chars": 386,
"preview": "describe('test', () => {\n it('should add third', () => {\n expect(5).toBe(6);\n });\n\n it('should add 1', () => {\n "
},
{
"path": "integration/projects/basic/test-only.spec.js",
"chars": 323,
"preview": "describe('describe', () => {\n it('it', () => {});\n test('test', () => {});\n});\n\ndescribe.only('describe.only', () => {"
},
{
"path": "integration/projects/basic/test-snapshot-failure.spec.js",
"chars": 328,
"preview": "import renderer from 'react-test-renderer';\nimport React from 'react';\nimport App from './app';\n\ndescribe('test', () => "
},
{
"path": "integration/projects/basic/test-snapshot-text.spec.js",
"chars": 126,
"preview": "describe('test', () => {\n it('Should not show snapshot button', () => {\n expect('snapshot').toBe('a snapshot');\n })"
},
{
"path": "nodemon.json",
"chars": 151,
"preview": "{\n \"ignore\": [\".git\", \"node_modules\"],\n \"watch\": [\"server\"],\n \"exec\": \"ts-node --project ./tsconfig.server.json ./ser"
},
{
"path": "package.json",
"chars": 3799,
"preview": "{\n \"name\": \"majestic\",\n \"version\": \"1.8.1\",\n \"engines\": {\n \"node\": \">=7.10.1\"\n },\n \"main\": \"index.js\",\n \"licens"
},
{
"path": "scripts/webpack.server.config.js",
"chars": 1257,
"preview": "const CopyPlugin = require('copy-webpack-plugin');\nconst webpack = require('webpack');\nconst path = require('path');\n\nmo"
},
{
"path": "scripts/webpack.ui.config.js",
"chars": 1646,
"preview": "const HtmlWebpackPlugin = require('html-webpack-plugin');\nconst webpack = require('webpack');\nconst path = require('path"
},
{
"path": "server/api/app/app.ts",
"chars": 139,
"preview": "import { ObjectType, Field } from \"type-graphql\";\n\n@ObjectType()\nexport class App {\n @Field({ nullable: true })\n selec"
},
{
"path": "server/api/app/resolver.ts",
"chars": 2347,
"preview": "import { Resolver, Mutation, Arg, Query } from \"type-graphql\";\nimport * as launch from \"launch-editor\";\nimport { App } f"
},
{
"path": "server/api/index.ts",
"chars": 348,
"preview": "import { buildSchema } from \"type-graphql\";\nimport { pubsub } from \"../event-emitter\";\nimport Workspace from \"./workspac"
},
{
"path": "server/api/runner/resolver.ts",
"chars": 3292,
"preview": "import {\n Resolver,\n Mutation,\n Arg,\n Query,\n Subscription,\n Root\n} from \"type-graphql\";\nimport { Runner } from \"."
},
{
"path": "server/api/runner/status.ts",
"chars": 251,
"preview": "import { ObjectType, Field, ID } from \"type-graphql\";\n\n@ObjectType()\nexport class RunnerStatus {\n @Field({ nullable: tr"
},
{
"path": "server/api/runner/type.ts",
"chars": 152,
"preview": "import { ObjectType, Field, ID } from \"type-graphql\";\n\n@ObjectType()\nexport class Runner {\n @Field()\n status: string;\n"
},
{
"path": "server/api/workspace/coverage.ts",
"chars": 308,
"preview": "import { ObjectType, Field } from \"type-graphql\";\n\n@ObjectType()\nexport class CoverageSummary {\n @Field({ nullable: tru"
},
{
"path": "server/api/workspace/resolver.ts",
"chars": 6870,
"preview": "import {\n Resolver,\n Arg,\n Query,\n Subscription,\n Root,\n Mutation\n} from \"type-graphql\";\nimport * as throttle from"
},
{
"path": "server/api/workspace/summary.ts",
"chars": 740,
"preview": "import { ObjectType, Field } from \"type-graphql\";\nimport { CoverageSummary } from \"./coverage\";\n\n@ObjectType()\nexport cl"
},
{
"path": "server/api/workspace/test-file.ts",
"chars": 184,
"preview": "import { ObjectType, Field } from \"type-graphql\";\nimport { TestItem } from \"./test-item\";\n\n@ObjectType()\nexport class Te"
},
{
"path": "server/api/workspace/test-item.ts",
"chars": 330,
"preview": "import { ObjectType, Field } from \"type-graphql\";\n\nexport type TestItemType = \"describe\" | \"it\" | \"todo\";\n\n@ObjectType()"
},
{
"path": "server/api/workspace/test-result/console-log.ts",
"chars": 235,
"preview": "import { ObjectType, Field } from \"type-graphql\";\n\n@ObjectType()\nexport class ConsoleLog {\n @Field({ nullable: true })\n"
},
{
"path": "server/api/workspace/test-result/file-result.ts",
"chars": 657,
"preview": "import { ObjectType, Field } from \"type-graphql\";\nimport { TestItemResult } from \"./test-item-result\";\nimport { ConsoleL"
},
{
"path": "server/api/workspace/test-result/test-item-result.ts",
"chars": 357,
"preview": "import { ObjectType, Field } from \"type-graphql\";\n\n@ObjectType()\nexport class TestItemResult {\n @Field()\n title: strin"
},
{
"path": "server/api/workspace/tree.ts",
"chars": 237,
"preview": "import { ObjectType, Field, ID } from \"type-graphql\";\n\n@ObjectType()\nexport class Item {\n @Field()\n path: string;\n\n @"
},
{
"path": "server/api/workspace/workspace.ts",
"chars": 232,
"preview": "import { ObjectType, Field, ID } from \"type-graphql\";\nimport { Item } from \"./tree\";\n\n@ObjectType()\nexport class Workspa"
},
{
"path": "server/event-emitter/index.ts",
"chars": 76,
"preview": "import { PubSub } from \"graphql-yoga\";\n\nexport const pubsub = new PubSub();\n"
},
{
"path": "server/index.ts",
"chars": 1682,
"preview": "import { GraphQLServer } from \"graphql-yoga\";\nimport \"reflect-metadata\";\nimport { getSchema } from \"./api\";\nimport resul"
},
{
"path": "server/logger.ts",
"chars": 478,
"preview": "declare var consola: any;\n\nexport function debugLog(tag: string, ...args: any) {\n if (process.env.DEBUG_LOG !== \"\") {\n "
},
{
"path": "server/services/ast/inspector.ts",
"chars": 3637,
"preview": "import traverse from \"@babel/traverse\";\nimport * as nanoid from \"nanoid\";\nimport { parse } from \"./parser\";\nimport { rea"
},
{
"path": "server/services/ast/parser.ts",
"chars": 400,
"preview": "import * as parser from \"@babel/parser\";\nimport { extname } from \"path\";\n\nexport function parse(path: string, code: stri"
},
{
"path": "server/services/cli.ts",
"chars": 55,
"preview": "export const root = process.env.ROOT || process.cwd();\n"
},
{
"path": "server/services/config-resolver.ts",
"chars": 3459,
"preview": "import * as parseArgs from \"minimist\";\nimport * as readPkgUp from \"read-pkg-up\";\nimport * as resolvePkg from \"resolve-pk"
},
{
"path": "server/services/file-watcher/index.ts",
"chars": 790,
"preview": "import { pubsub } from \"../../event-emitter\";\nimport { watch } from \"fs\";\nimport { createLogger } from \"../../logger\";\n\n"
},
{
"path": "server/services/jest-manager/cli-args.ts",
"chars": 42,
"preview": "export const ShowConfig = \"--showConfig\";\n"
},
{
"path": "server/services/jest-manager/index.ts",
"chars": 5319,
"preview": "import { spawn, ChildProcess, execSync } from \"child_process\";\nimport { join } from \"path\";\nimport Project from \"../proj"
},
{
"path": "server/services/jest-manager/scripts/patch.js",
"chars": 128,
"preview": "// Monkey patch the stdin with setRawMode so jest would think it's running from a terminal\nprocess.stdin.setRawMode = ()"
},
{
"path": "server/services/jest-manager/scripts/reporter.js",
"chars": 1730,
"preview": "const fetch = require('node-fetch');\n\nfunction send(type, body) {\n fetch('http://localhost:' + process.env.MAJESTIC_POR"
},
{
"path": "server/services/project.ts",
"chars": 1893,
"preview": "import { TreeMap, MajesticConfig } from \"./types\";\nimport { spawnSync } from \"child_process\";\nimport { sep, join, extnam"
},
{
"path": "server/services/result-handler-api.ts",
"chars": 1842,
"preview": "import { Application } from \"express\";\nimport * as bodyParser from \"body-parser\";\nimport { pubsub } from \"../event-emitt"
},
{
"path": "server/services/results.ts",
"chars": 5803,
"preview": "import { createSourceMapStore, MapStore } from \"istanbul-lib-source-maps\";\nimport { createCoverageMap, CoverageMap } fro"
},
{
"path": "server/services/types.ts",
"chars": 388,
"preview": "export interface DirectoryItem {\n name: string;\n path: string;\n type: \"directory\" | \"file\";\n children?: DirectoryIte"
},
{
"path": "server/static-files.ts",
"chars": 920,
"preview": "import * as exp from \"express\";\nimport { resolve, join } from \"path\";\nimport { pubsub } from \"./event-emitter\";\n\nexport "
},
{
"path": "server/typings.d.ts",
"chars": 265,
"preview": "declare module \"directory-tree\";\ndeclare module \"micromatch\";\ndeclare module \"@babel/traverse\";\ndeclare module \"nanoid\";"
},
{
"path": "tsconfig.json",
"chars": 607,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"es2015\",\n \"allowSyntheticDefaultImports\": true,\n \"target\": \"es5\",\n \"lib"
},
{
"path": "tsconfig.server.json",
"chars": 715,
"preview": "{\n \"compilerOptions\": {\n \"rootDir\": \"./server\",\n \"outDir\": \"./dist/server\",\n \"module\": \"commonjs\",\n \"target"
},
{
"path": "ui/apollo-client.ts",
"chars": 1051,
"preview": "import { ApolloClient, HttpLink, InMemoryCache } from \"apollo-client-preset\";\nimport { WebSocketLink } from \"apollo-link"
},
{
"path": "ui/app.gql",
"chars": 33,
"preview": "{\n app {\n selectedFile\n }\n}\n"
},
{
"path": "ui/app.tsx",
"chars": 4241,
"preview": "import React, { useState } from \"react\";\nimport styled from \"styled-components\";\nimport SplitPane from \"react-split-pane"
},
{
"path": "ui/components/button.tsx",
"chars": 914,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { space, color, fontSize } from \"styled-system"
},
{
"path": "ui/container.tsx",
"chars": 1197,
"preview": "import React, { Component, Suspense } from \"react\";\nimport { ApolloProvider as ApolloHooksProvider } from \"react-apollo-"
},
{
"path": "ui/coverage-panel/index.tsx",
"chars": 339,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { getAPIUrl } from \"../apollo-client\";\n\nconst "
},
{
"path": "ui/error.tsx",
"chars": 1858,
"preview": "import React, { Component } from \"react\";\nimport styled from \"styled-components\";\n\nconst Container = styled.div`\n displ"
},
{
"path": "ui/hooks/use-keys.ts",
"chars": 1035,
"preview": "import { useEffect, useState } from \"react\";\n\nexport function hasKeys(expectedKeys: string[], pressedKeys: Map<String, b"
},
{
"path": "ui/index.tsx",
"chars": 436,
"preview": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport \"@babel/polyfill\";\nimport Container from \"./containe"
},
{
"path": "ui/loading.tsx",
"chars": 1580,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\n\nconst Container = styled.div`\n display: flex;\n fle"
},
{
"path": "ui/query.gql",
"chars": 111,
"preview": "{\n workspace {\n projectRoot\n name\n files {\n path\n name\n type\n parent\n }\n }\n}\n"
},
{
"path": "ui/runner-status-query.gql",
"chars": 65,
"preview": "{\n runnerStatus {\n running\n activeFile\n watching\n }\n}\n"
},
{
"path": "ui/runner-status-subs.gql",
"chars": 84,
"preview": "subscription {\n runnerStatusChange {\n running\n activeFile\n watching\n }\n}\n"
},
{
"path": "ui/search/index.tsx",
"chars": 2641,
"preview": "import React, { useEffect, useRef, useState } from \"react\";\nimport styled from \"styled-components\";\nimport { Item } from"
},
{
"path": "ui/set-selected-file.gql",
"chars": 98,
"preview": "mutation SetSelectedFile($path: String) {\n setSelectedFile(path: $path) {\n selectedFile\n }\n}\n"
},
{
"path": "ui/sidebar/execution-indicator.tsx",
"chars": 1075,
"preview": "import React from \"react\";\n\nexport default function ExecutionIndicator() {\n return (\n <svg\n version=\"1.1\"\n "
},
{
"path": "ui/sidebar/file-item.tsx",
"chars": 2613,
"preview": "import React, { memo } from \"react\";\nimport styled from \"styled-components\";\nimport {\n File,\n Folder,\n ChevronRight,\n"
},
{
"path": "ui/sidebar/index.tsx",
"chars": 8255,
"preview": "import React, { useState } from \"react\";\nimport styled from \"styled-components\";\nimport { useMutation, useQuery } from \""
},
{
"path": "ui/sidebar/logo.tsx",
"chars": 326,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport logo from \"../assets/logo.png\";\n\nconst Contain"
},
{
"path": "ui/sidebar/run.gql",
"chars": 19,
"preview": "mutation {\n run\n}\n"
},
{
"path": "ui/sidebar/set-collect-coverage.gql",
"chars": 92,
"preview": "mutation SetCollectCoverage($collect: Boolean!) {\n setCollectCoverage(collect: $collect)\n}\n"
},
{
"path": "ui/sidebar/set-watch-mode.gql",
"chars": 92,
"preview": "mutation SetWatchMode($watch: Boolean!) {\n toggleWatch(watch: $watch) {\n watching\n }\n}\n"
},
{
"path": "ui/sidebar/should-collect-coverage.gql",
"chars": 28,
"preview": "{\n shouldCollectCoverage\n}\n"
},
{
"path": "ui/sidebar/summary/index.tsx",
"chars": 4402,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { space } from \"styled-system\";\nimport { useSp"
},
{
"path": "ui/sidebar/transformer.ts",
"chars": 2329,
"preview": "import { Item } from \"../../server/api/workspace/tree\";\n\nexport interface TreeNode extends Item {\n name: string;\n path"
},
{
"path": "ui/sidebar/tree.tsx",
"chars": 1370,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { FixedSizeList as List } from \"react-window\";"
},
{
"path": "ui/split-panel-style.ts",
"chars": 992,
"preview": "const splitPanelCSS = `\n.Resizer {\n background: #404148;\n opacity: .8;\n z-index: 1;\n box-sizing: border-box;"
},
{
"path": "ui/stop-runner.gql",
"chars": 20,
"preview": "mutation {\n stop\n}\n"
},
{
"path": "ui/summary-query.gql",
"chars": 263,
"preview": "query {\n summary {\n numPassedTests\n numFailedTests\n numPassedTestSuites\n numFailedTestSuites\n failedTest"
},
{
"path": "ui/summary-subscription.gql",
"chars": 278,
"preview": "subscription {\n changeToSummary {\n numPassedTests\n numFailedTests\n numPassedTestSuites\n numFailedTestSuites"
},
{
"path": "ui/test-file/console-panel/index.tsx",
"chars": 2482,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { ObjectInspector, chromeDark } from \"react-in"
},
{
"path": "ui/test-file/error-panel/index.tsx",
"chars": 886,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport * as Convert from \"ansi-to-html\";\n\nconst conve"
},
{
"path": "ui/test-file/file-items-subscription.gql",
"chars": 138,
"preview": "subscription($path: String!) {\n fileChange(path: $path) {\n items {\n id\n name\n type\n parent\n "
},
{
"path": "ui/test-file/index.tsx",
"chars": 4329,
"preview": "import React, { memo } from \"react\";\nimport styled from \"styled-components\";\nimport { space, color } from \"styled-system"
},
{
"path": "ui/test-file/open-failure.gql",
"chars": 77,
"preview": "mutation OpenFailure($failure: String!) {\n openFailure(failure: $failure)\n}\n"
},
{
"path": "ui/test-file/query.gql",
"chars": 135,
"preview": "query FileItems($path: String!) {\n file(path: $path) {\n items {\n id\n name\n type\n parent\n on"
},
{
"path": "ui/test-file/result.gql",
"chars": 323,
"preview": "query Results($path: String!) {\n result(path: $path) {\n path\n numFailingTests\n numPassingTests\n failureMess"
},
{
"path": "ui/test-file/run-file.gql",
"chars": 60,
"preview": "mutation RunFile($path: String!) {\n runFile(path: $path)\n}\n"
},
{
"path": "ui/test-file/subscription.gql",
"chars": 338,
"preview": "subscription Results($path: String!) {\n changeToResult(path: $path) {\n path\n numFailingTests\n numPassingTests\n"
},
{
"path": "ui/test-file/summary/index.tsx",
"chars": 5063,
"preview": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { space, fontSize, color } from \"styled-system"
},
{
"path": "ui/test-file/summary/open-in-editor.gql",
"chars": 70,
"preview": "mutation OpenInEditor($path: String!) {\n openInEditor(path: $path)\n}\n"
},
{
"path": "ui/test-file/summary/open-snap-in-editor.gql",
"chars": 78,
"preview": "mutation OpenSnapInEditor($path: String!) {\n openSnapInEditor(path: $path)\n}\n"
},
{
"path": "ui/test-file/test-indicator.tsx",
"chars": 791,
"preview": "import React from \"react\";\nimport {\n CheckCircle,\n Circle,\n Package,\n XCircle,\n Zap,\n Edit2\n} from \"react-feather\""
},
{
"path": "ui/test-file/test-item.tsx",
"chars": 3506,
"preview": "import React, { Fragment } from \"react\";\nimport styled from \"styled-components\";\nimport { TestFileItem } from \"./transfo"
},
{
"path": "ui/test-file/transformer.ts",
"chars": 856,
"preview": "import { TestItem } from \"../../server/api/workspace/test-item\";\n\nexport interface TestFileItem extends TestItem {\n chi"
},
{
"path": "ui/test-file/update-snapshot.gql",
"chars": 74,
"preview": "mutation UpdateSnapshot($path: String!) {\n updateSnapshot(path: $path)\n}\n"
},
{
"path": "ui/test-file/use-subscription.tsx",
"chars": 2073,
"preview": "import React, { useState, useEffect } from \"react\";\nimport { DocumentNode } from \"graphql\";\nimport { useApolloClient } f"
},
{
"path": "ui/theme.ts",
"chars": 250,
"preview": "export default {\n colors: {\n veryDark: \"#262529\",\n dark: \"#242326\",\n slightDark: \"#404148\",\n text: \"#F5F5F5"
},
{
"path": "ui/typings.d.ts",
"chars": 466,
"preview": "declare module \"apollo-link-ws\";\ndeclare module \"apollo-utilities\";\ndeclare module \"apollo-link\";\ndeclare module \"apollo"
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the Raathigesh/majestic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 120 files (139.4 KB), approximately 39.8k tokens, and a symbol index with 168 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.