├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── .nycrc ├── .prettierrc ├── .yarnrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── for-readme └── test-angular.jpg ├── lerna.json ├── package.json ├── packages ├── jasmine-single │ ├── .all-contributorsrc │ ├── .core-version │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── jasmine.json │ ├── package.json │ ├── spec │ │ └── jasmine-single.spec.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── jest-single │ ├── .all-contributorsrc │ ├── .core-version │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── spec │ │ └── jest-single.spec.ts │ ├── tsconfig.build.json │ └── tsconfig.json └── karma-jasmine-single │ ├── .all-contributorsrc │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── karma.conf.js │ ├── lib │ ├── karma-jasmine-single.js │ └── karma-jasmine-single.spec.js │ └── package.json ├── shared └── single-core │ └── single-core.ts ├── update-core-hash.js └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "single", 3 | "projectOwner": "hirezio", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "shairez", 15 | "name": "Shai Reznik", 16 | "avatar_url": "https://avatars1.githubusercontent.com/u/1430726?v=4", 17 | "profile": "https://www.hirez.io/?utm_medium=Open_Source&utm_source=Github&utm_campaign=Lead_Generation&utm_content=single--all-contributors-profile-link", 18 | "contributions": [ 19 | "code", 20 | "doc", 21 | "ideas", 22 | "infra", 23 | "maintenance", 24 | "mentoring", 25 | "review", 26 | "test" 27 | ] 28 | }, 29 | { 30 | "login": "maartentibau", 31 | "name": "Maarten Tibau", 32 | "avatar_url": "https://avatars.githubusercontent.com/u/4103756?v=4", 33 | "profile": "https://www.webtrix.be", 34 | "contributions": [ 35 | "doc", 36 | "infra" 37 | ] 38 | }, 39 | { 40 | "login": "benjamingr", 41 | "name": "Benjamin Gruenbaum", 42 | "avatar_url": "https://avatars.githubusercontent.com/u/1315533?v=4", 43 | "profile": "https://stackoverflow.com/users/1348195/benjamin-gruenbaum", 44 | "contributions": [ 45 | "code", 46 | "ideas", 47 | "maintenance" 48 | ] 49 | }, 50 | { 51 | "login": "awarrington0895", 52 | "name": "Alex ", 53 | "avatar_url": "https://avatars.githubusercontent.com/u/12807806?v=4", 54 | "profile": "https://github.com/awarrington0895", 55 | "contributions": [ 56 | "code", 57 | "maintenance" 58 | ] 59 | } 60 | ], 61 | "contributorsPerLine": 7, 62 | "skipCi": true, 63 | "commitType": "docs" 64 | } 65 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .eslintrc.* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | "prettier" 11 | ], 12 | rules: { 13 | '@typescript-eslint/no-inferrable-types': "off", 14 | '@typescript-eslint/no-explicit-any': "off", 15 | 'no-undef': "off" 16 | }, 17 | overrides: [ 18 | { 19 | files: ["**/*.spec.ts"], 20 | rules: { 21 | "@typescript-eslint/no-explicit-any": "off", 22 | "@typescript-eslint/no-unused-vars": "off", 23 | "@typescript-eslint/ban-types": "off", 24 | "@typescript-eslint/no-empty-function": "off" 25 | } 26 | } 27 | ], 28 | 29 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - staging 7 | pull_request: 8 | jobs: 9 | build: 10 | name: Build and test on node ${{ matrix.node }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node: ['14', '16'] 15 | env: 16 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 18 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | steps: 20 | - name: Checkout Repo 21 | uses: actions/checkout@v2 22 | with: 23 | fetch-depth: '0' 24 | - name: Setup Node 25 | uses: actions/setup-node@v2-beta 26 | with: 27 | node-version: ${{ matrix.node }} 28 | registry-url: 'https://registry.npmjs.org' 29 | - name: Get yarn cache dir 30 | id: yarn-cache-dir-path 31 | run: echo "::set-output name=dir::$(yarn cache dir)" 32 | 33 | - name: Cache Dependencies 34 | uses: actions/cache@v2 35 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 36 | with: 37 | path: | 38 | ${{ steps.yarn-cache-dir-path.outputs.dir }} 39 | node_modules 40 | */*/node_modules 41 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 42 | restore-keys: | 43 | ${{ runner.os }}-yarn- 44 | 45 | - name: Install dependencies 46 | run: yarn install --frozen-lockfile 47 | 48 | - name: Build 49 | run: yarn build 50 | 51 | - name: Run Lint and Tests 52 | run: yarn test:full 53 | 54 | release: 55 | name: Release 56 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 57 | needs: build 58 | runs-on: ubuntu-latest 59 | 60 | env: 61 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 63 | steps: 64 | - name: Checkout Repo 65 | uses: actions/checkout@v2 66 | with: 67 | fetch-depth: '0' 68 | 69 | - name: Setup Node 70 | uses: actions/setup-node@v2 71 | with: 72 | node-version: '16' 73 | registry-url: 'https://registry.npmjs.org' 74 | scope: '@hirez_io' 75 | 76 | - name: Get yarn cache dir 77 | id: yarn-cache-dir-path 78 | run: echo "::set-output name=dir::$(yarn cache dir)" 79 | 80 | - name: Cache Dependencies 81 | uses: actions/cache@v2 82 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 83 | with: 84 | path: | 85 | ${{ steps.yarn-cache-dir-path.outputs.dir }} 86 | node_modules 87 | */*/node_modules 88 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 89 | restore-keys: | 90 | ${{ runner.os }}-yarn- 91 | 92 | - name: Install dependencies 93 | run: yarn install --frozen-lockfile 94 | 95 | - name: Build 96 | run: yarn build 97 | 98 | - name: Run Lint and Tests 99 | run: yarn test:full 100 | 101 | - name: Upload coverage reports 102 | uses: codecov/codecov-action@v1 103 | with: 104 | files: ./packages/jasmine-single/coverage/lcov.info, ./packages/jest-single/coverage/lcov.info 105 | 106 | - name: Configure CI Git User 107 | run: | 108 | git config --global user.name '@hirezio' 109 | git config --global user.email 'hirezio@users.noreply.github.com' 110 | 111 | - name: Update Version 112 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 113 | run: yarn lerna:version:ci 114 | 115 | - name: Check Authentication with Registry 116 | env: 117 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 118 | run: npm whoami 119 | 120 | - name: Publish to npm 121 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 122 | env: 123 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 124 | run: yarn lerna:publish 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | temp-src 4 | coverage 5 | .nyc_output 6 | src/**/*.js* 7 | **/*.d.ts 8 | .vscode 9 | .cache 10 | .idea 11 | TODO 12 | yarn-error.log 13 | lerna-debug.log -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "report-dir": "packages/jasmine-single/coverage", 3 | "temp-dir": "packages/jasmine-single/.nyc_output", 4 | "check-coverage": true, 5 | 6 | "include": [ 7 | "shared/single-core/single-core.ts" 8 | ], 9 | "branches": 100, 10 | "functions": 90, 11 | "lines": 90, 12 | "statements": 90 13 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 90, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true 8 | } -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix "" -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | conduct@hirez.io. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 121 | 122 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 123 | enforcement ladder](https://github.com/mozilla/diversity). 124 | 125 | [homepage]: https://www.contributor-covenant.org 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | https://www.contributor-covenant.org/faq. Translations are available at 129 | https://www.contributor-covenant.org/translations. 130 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | We would love for you to contribute to this project. 4 | As a contributor, here are the guidelines we would like you to follow: 5 | 6 | ## Be Kind - Code of Conduct 7 | 8 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md) 9 | 10 | ## Found a bug? Want a feature? - Please submit an Issue 11 | 12 | [Choose an issue template](https://github.com/hirezio/single/issues/new/choose) to file a bug report / feature request. 13 | 14 | ## Want to contribute code? please submit a Pull Request (PR), but first... 15 | 16 | . 17 | 18 | ### ✅ 1. [Search this repo first](https://github.com/hirezio/single/pulls)... 19 | 20 | for an open or closed PR that relates to the change you want to introduce. 21 | 22 | . 23 | 24 | ### ✅ 2. **Before you start coding - find / create an issue** 25 | 26 | **Make sure there's an issue** describing the problem you're fixing, or documents the design for the feature you'd like to add. 27 | Discussing the design up front helps to ensure that we're ready to accept your work. 28 | 29 | **Don't waste your time working on code before you got a 👍 in an issue comment.** 30 | 31 | . 32 | 33 | ### ✅ 3. Fork the this repo and create a branch. 34 | 35 | - Hit that "Fork" button above (in this repo's github page). 36 | 37 | ![image](https://user-images.githubusercontent.com/1430726/95460679-ec014400-097d-11eb-9a7a-93e0262d37d9.png) 38 | 39 | - git clone your fork 40 | 41 | `git clone YOUR_FORK_URL` 42 | 43 | Get your url by from here 👇 44 | 45 | ![image](https://user-images.githubusercontent.com/1430726/95461173-94afa380-097e-11eb-9568-dc986e050de6.png) 46 | 47 | - Create a new branch locally in your fork's repo 48 | 49 | ```shell 50 | git checkout -b my-fix-branch main 51 | ``` 52 | 53 | ⚠ **IMPORTANT:** In this project the most important file is `shared/single-core/single-core.ts` - 54 | It is the actual implementation of both `jasmine-single` and `jest-single`. 55 | So if you change anything you need to verify that there is a test in both of the frameworks spec files. 56 | 57 | . 58 | 59 | ### ✅ 4. Make sure you add / modify tests 60 | 61 | Run `yarn test:full` to make sure there aren't any errors 62 | 63 | . 64 | 65 | ### ✅ 5. Commit your changes using commitizen: 66 | 67 | Instead of `git commit` use the following command: 68 | 69 | ```shell 70 | yarn commit 71 | ``` 72 | 73 | It will then ask you a bunch of questions. 74 | 75 | **For "scope" please choose from the following options:** 76 | 77 | | Scope name | Description | 78 | | ------------------- | --------------------------------------------------- | 79 | | core | a change related to the file `single-core.ts` | 80 | | jest-single | a change related to `@hirez_io/jest-single` | 81 | | jasmine-single | a change related to `@hirez_io/jasmine-single` | 82 | | karma-jasmine-single | a change related to `@hirez_io/karma-jasmine-single` | 83 | | global | any change that doesn't fall under the above scopes | 84 | 85 | This will create a descriptive commit message that follows the 86 | [Angular commit message convention](#commit-message-format). 87 | 88 | This is necessary to generate meaningful release notes / CHANGELOG automatically. 89 | 90 | . 91 | 92 | ### ✅ 6. Push your branch to GitHub: 93 | 94 | ```shell 95 | git push origin my-fix-branch 96 | ``` 97 | 98 | . 99 | 100 | ### ✅ 7. In GitHub, create a pull request for `hirezio/single:main`. 101 | 102 | Make sure you check the following checkbox "Allow edits from maintainers" - 103 | 104 | ![image](https://user-images.githubusercontent.com/1430726/95461503-fbcd5800-097e-11eb-9b55-321d1ff0e6bb.png) 105 | 106 | If you need to update your PR for some reason - 107 | 108 | - Make the required updates. 109 | 110 | - Re-run the tests to ensure tests are still passing `yarn test:full` 111 | 112 | - Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 113 | 114 | ```shell 115 | git rebase main -i 116 | git push -f 117 | ``` 118 | 119 | . 120 | 121 | ### ✅ 8. After your pull request is merged - delete your PR branch 122 | 123 | After your pull request is merged, you can safely delete your branch and pull the changes from the main (upstream) repository: 124 | 125 | - Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 126 | 127 | ```shell 128 | git push origin --delete my-fix-branch 129 | ``` 130 | 131 | - Check out the main branch: 132 | 133 | ```shell 134 | git checkout main -f 135 | ``` 136 | 137 | - Delete the local branch: 138 | 139 | ```shell 140 | git branch -D my-fix-branch 141 | ``` 142 | 143 | - Update your main with the latest upstream version: 144 | 145 | ```shell 146 | git pull --ff upstream main 147 | ``` 148 | 149 | . 150 | 151 | ### ✅ 9. That's it! Thank you for your contribution! 🙏💓 152 | 153 | [commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# 154 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Shai Reznik, HiRez.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (jasmine | jest)-single Monorepo 2 | 3 | 4 | [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) 5 | 6 | 7 | ## Packages 8 | 9 | This repository contains the HiRez.io's versions of: 10 | 11 | | Project | Status | Description | 12 | | ------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------- | 13 | | [@hirez_io/jest-single] | [![@hirez_io/jest-single-status]][@hirez_io/jest-single-package] | Jest addon that adds the "given when then" syntax | 14 | | [@hirez_io/jasmine-single] | [![@hirez_io/jasmine-single-status]][@hirez_io/jasmine-single-package] | Jasmine addon that adds the "given when then" syntax | 15 | | [@hirez_io/karma-jasmine-single] | [![@hirez_io/karma-jasmine-single-status]][@hirez_io/karma-jasmine-single-package] | Karma plugin for `@hirez_io/jasmine-single` | 16 | 17 | [@hirez_io/jest-single]: https://github.com/hirezio/single/tree/main/packages/jest-single 18 | [@hirez_io/jasmine-single]: https://github.com/hirezio/single/tree/main/packages/jasmine-single 19 | [@hirez_io/karma-jasmine-single]: https://github.com/hirezio/single/tree/main/packages/karma-jasmine-single 20 | [@hirez_io/jest-single-status]: https://img.shields.io/npm/v/@hirez_io/jest-single.svg 21 | [@hirez_io/jest-single-package]: https://npmjs.com/package/@hirez_io/jest-single 22 | [@hirez_io/jasmine-single-status]: https://img.shields.io/npm/v/@hirez_io/jasmine-single.svg 23 | [@hirez_io/jasmine-single-package]: https://npmjs.com/package/@hirez_io/jasmine-single 24 | [@hirez_io/karma-jasmine-single-status]: https://img.shields.io/npm/v/@hirez_io/karma-jasmine-single.svg 25 | [@hirez_io/karma-jasmine-single-package]: https://npmjs.com/package/@hirez_io/karma-jasmine-single 26 | 27 | ## Contributing 28 | 29 | Want to contribute? Yayy! 🎉 30 | 31 | Please read and follow our [Contributing Guidelines](CONTRIBUTING.md) to learn what are the right steps to take before contributing your time, effort and code. 32 | 33 | Thanks 🙏 34 | 35 | ## Code Of Conduct 36 | 37 | Be kind to each other and please read our [code of conduct](CODE_OF_CONDUCT.md). 38 | 39 | 40 |
41 | 42 | ## Contributors ✨ 43 | 44 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
Shai Reznik
Shai Reznik

💻 📖 🤔 🚇 🚧 🧑‍🏫 👀 ⚠️
Maarten Tibau
Maarten Tibau

📖 🚇
Benjamin Gruenbaum
Benjamin Gruenbaum

💻 🤔 🚧
Alex
Alex

💻 🚧
59 | 60 | 61 | 62 | 63 | 64 | 65 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 66 | 67 |
68 | 69 | ## License 70 | 71 | MIT 72 | 73 | ## Want to learn more? 74 | 75 |
76 | 77 | TestAngular.com - Free Angular Testing Workshop - The Roadmap to Angular Testing Mastery 81 | 82 |
83 | -------------------------------------------------------------------------------- /for-readme/test-angular.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirezio/single/9a0d2ef38d2a07fadb7fb3be5fe6e1cf226b084d/for-readme/test-angular.jpg -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "version": "independent", 6 | "command": { 7 | "publish": { 8 | "conventionalCommits": true 9 | }, 10 | "version": { 11 | "conventionalCommits": true, 12 | "conventionalGraduate":"*", 13 | "push": true, 14 | "gitTagVersion": true, 15 | "createRelease": "github", 16 | "allowBranch": "main", 17 | "message": "chore(release): publish new version" 18 | } 19 | }, 20 | "ignoreChanges": ["**/*.md", "**/*.spec.ts", "**/*.spec.js", "**/.all-contributorsrc"] 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "single", 3 | "version": "0.0.0", 4 | "author": { 5 | "name": "Shai Reznik", 6 | "company": "HiRez.io" 7 | }, 8 | "license": "MIT", 9 | "private": true, 10 | "scripts": { 11 | "commit": "git-cz", 12 | "build": "lerna run build", 13 | "format:fix": "pretty-quick --staged", 14 | "lerna:publish": "lerna publish from-git --yes", 15 | "lerna:version": "lerna version", 16 | "lerna:version:ci": "lerna version --yes", 17 | "lint": "eslint . --ext .js,.ts", 18 | "lint:commitmsg": "commitlint -E HUSKY_GIT_PARAMS", 19 | "test": "run-s test:jasmine:coverage test:jest", 20 | "test:full": "run-s lint test", 21 | "test:jasmine": "ts-node --project packages/jasmine-single/tsconfig.json -r tsconfig-paths/register node_modules/jasmine/bin/jasmine.js JASMINE_CONFIG_PATH=packages/jasmine-single/jasmine.json", 22 | "test:jasmine:coverage": "nyc -r lcov --r text-summary -e .ts -x \"**/*.spec.ts\" yarn test:jasmine", 23 | "test:jasmine:watch": "nodemon --ext ts --watch \"shared/single-core/**/*.ts\" --watch \"packages/jasmine-single/**/*.ts\" --exec \"yarn test:jasmine:coverage\"", 24 | "test:jest": "jest -c packages/jest-single/jest.config.js", 25 | "test:jest:watch": "jest -c packages/jest-single/jest.config.js --watchAll", 26 | "update-core-hash": "node update-core-hash" 27 | }, 28 | "workspaces": [ 29 | "packages/*" 30 | ], 31 | "config": { 32 | "commitizen": { 33 | "path": "./node_modules/cz-conventional-changelog" 34 | } 35 | }, 36 | "husky": { 37 | "hooks": { 38 | "pre-commit": "yarn format:fix", 39 | "commit-msg": "yarn lint:commitmsg", 40 | "post-commit": "yarn update-core-hash" 41 | } 42 | }, 43 | "commitlint": { 44 | "extends": [ 45 | "@commitlint/config-conventional" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@commitlint/cli": "16.2.1", 50 | "@commitlint/config-conventional": "16.2.1", 51 | "@types/node": "17.0.21", 52 | "@typescript-eslint/eslint-plugin": "5.14.0", 53 | "@typescript-eslint/parser": "5.14.0", 54 | "all-contributors-cli": "6.20.0", 55 | "commitizen": "4.2.4", 56 | "cz-conventional-changelog": "3.3.0", 57 | "doctoc": "2.1.0", 58 | "eslint": "8.11.0", 59 | "eslint-config-prettier": "8.5.0", 60 | "execa": "6.1.0", 61 | "husky": "7.0.4", 62 | "lerna": "^4.0.0", 63 | "npm-run-all": "^4.1.5", 64 | "prettier": "2.5.1", 65 | "pretty-quick": "3.1.3", 66 | "rimraf": "^3.0.2", 67 | "ts-node": "10.7.0", 68 | "typescript": "4.6.2" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/jasmine-single/.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "single", 3 | "projectOwner": "hirezio", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md", 8 | "../../README.md" 9 | ], 10 | "imageSize": 100, 11 | "commit": true, 12 | "commitConvention": "angular", 13 | "contributors": [ 14 | { 15 | "login": "shairez", 16 | "name": "Shai Reznik", 17 | "avatar_url": "https://avatars1.githubusercontent.com/u/1430726?v=4", 18 | "profile": "https://www.hirez.io/?utm_medium=Open_Source&utm_source=Github&utm_campaign=Lead_Generation&utm_content=jasmine-single--all-contributors-profile-link", 19 | "contributions": [ 20 | "code", 21 | "doc", 22 | "ideas", 23 | "infra", 24 | "maintenance", 25 | "mentoring", 26 | "review", 27 | "test" 28 | ] 29 | }, 30 | { 31 | "login": "maartentibau", 32 | "name": "Maarten Tibau", 33 | "avatar_url": "https://avatars.githubusercontent.com/u/4103756?v=4", 34 | "profile": "https://www.webtrix.be", 35 | "contributions": [ 36 | "doc", 37 | "infra" 38 | ] 39 | }, 40 | { 41 | "login": "benjamingr", 42 | "name": "Benjamin Gruenbaum", 43 | "avatar_url": "https://avatars.githubusercontent.com/u/1315533?v=4", 44 | "profile": "https://stackoverflow.com/users/1348195/benjamin-gruenbaum", 45 | "contributions": [ 46 | "code", 47 | "ideas", 48 | "maintenance" 49 | ] 50 | } 51 | ], 52 | "contributorsPerLine": 7 53 | } 54 | -------------------------------------------------------------------------------- /packages/jasmine-single/.core-version: -------------------------------------------------------------------------------- 1 | 3f24aa9551ab834f07b3464b2043721fb3b55edf -------------------------------------------------------------------------------- /packages/jasmine-single/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output -------------------------------------------------------------------------------- /packages/jasmine-single/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.1.6](https://github.com/hirezio/single/compare/@hirez_io/jasmine-single@1.1.5...@hirez_io/jasmine-single@1.1.6) (2023-08-25) 7 | 8 | **Note:** Version bump only for package @hirez_io/jasmine-single 9 | 10 | 11 | 12 | 13 | 14 | ## [1.1.5](https://github.com/hirezio/single/compare/@hirez_io/jasmine-single@1.1.4...@hirez_io/jasmine-single@1.1.5) (2022-06-04) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * **global:** fix fgiven description ([dc75bd9](https://github.com/hirezio/single/commit/dc75bd9ff24b6f4f5b50ed734e1ffed99b9c46ae)) 20 | * **global:** fix unhandled promise exceptions bug ([06f4934](https://github.com/hirezio/single/commit/06f4934c607aeec06520874a2563e2bd002a1337)) 21 | 22 | 23 | 24 | 25 | 26 | ## [1.1.4](https://github.com/hirezio/single/compare/@hirez_io/jasmine-single@1.1.3...@hirez_io/jasmine-single@1.1.4) (2022-03-13) 27 | 28 | **Note:** Version bump only for package @hirez_io/jasmine-single 29 | 30 | 31 | 32 | 33 | 34 | ## [1.1.3](https://github.com/hirezio/single/compare/@hirez_io/jasmine-single@1.1.2...@hirez_io/jasmine-single@1.1.3) (2022-03-13) 35 | 36 | **Note:** Version bump only for package @hirez_io/jasmine-single 37 | 38 | 39 | 40 | 41 | 42 | ## [1.1.2](https://github.com/hirezio/single/compare/@hirez_io/jasmine-single@1.1.1...@hirez_io/jasmine-single@1.1.2) (2022-03-02) 43 | 44 | **Note:** Version bump only for package @hirez_io/jasmine-single 45 | 46 | 47 | 48 | 49 | 50 | ## [1.1.1](https://github.com/hirezio/single/compare/@hirez_io/jasmine-single@1.1.0...@hirez_io/jasmine-single@1.1.1) (2021-08-08) 51 | 52 | **Note:** Version bump only for package @hirez_io/jasmine-single 53 | 54 | 55 | 56 | 57 | 58 | # [1.1.0](https://github.com/hirezio/single/compare/@hirez_io/jasmine-single@1.0.0...@hirez_io/jasmine-single@1.1.0) (2021-08-08) 59 | 60 | 61 | ### Features 62 | 63 | * **global:** add support for `done` and update README ([6082c27](https://github.com/hirezio/single/commit/6082c2710153ea0a5288a25457a7a78828a7b48d)) 64 | 65 | 66 | 67 | 68 | 69 | # 1.0.0 (2021-07-29) 70 | 71 | **Note:** Version bump only for package @hirez_io/jasmine-single 72 | -------------------------------------------------------------------------------- /packages/jasmine-single/README.md: -------------------------------------------------------------------------------- 1 | # `@hirez_io/jasmine-single` 📃👌 2 | 3 | A jasmine addon that helps you write 'Single-Action Tests' by breaking them into a **"given / when / then"** structure. 4 | 5 | [![npm version](https://img.shields.io/npm/v/@hirez_io/jasmine-single.svg?style=flat-square)](https://www.npmjs.org/package/@hirez_io/jasmine-single) 6 | [![npm downloads](https://img.shields.io/npm/dm/@hirez_io/jasmine-single.svg?style=flat-square)](http://npm-stat.com/charts.html?package=@hirez_io/jasmine-single&from=2017-07-26) 7 | [![codecov](https://img.shields.io/codecov/c/github/hirezio/single.svg)](https://codecov.io/gh/hirezio/single) 8 | ![Build and optionally publish](https://github.com/hirezio/single/workflows/Build%20and%20optionally%20publish/badge.svg) 9 | [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) 10 | [![Code of Conduct](https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square)](../../CODE_OF_CONDUCT.md) 11 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 12 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-green.svg?style=flat-square)](#contributors-) 13 | 14 | 15 |
16 | 17 | TestAngular.com - Free Angular Testing Workshop - The Roadmap to Angular Testing Mastery 21 | 22 |
23 | 24 |
25 | 26 | # Table of Contents 27 | 28 | 29 | 30 | 31 | - [Installation](#installation) 32 | 33 | - [Using TypeScript?](#using-typescript) 34 | 35 | - [Using karma?](#using-karma) 36 | 37 | - [What are "single-action" tests?](#what-are-single-action-tests) 38 | 39 | - [Why writing single-action tests is good?](#why-writing-single-action-tests-is-good) 40 | 41 | - [How to write single-action tests?](#how-to-write-single-action-tests) 42 | 43 | - [What's wrong with using `it()` for single-action tests?](#whats-wrong-with-using-it-for-single-action-tests) 44 | 45 | - [Usage](#usage) 46 | 47 | - [▶ The basic testing structure](#%E2%96%B6-the-basic-testing-structure) 48 | 49 | - [▶ Meaningful error messages](#%E2%96%B6-meaningful-error-messages) 50 | 51 | - [▶ `async` / `await` support](#%E2%96%B6-async--await-support) 52 | 53 | - [▶ `done()` function support](#%E2%96%B6-done-function-support) 54 | - [Contributing](#contributing) 55 | - [Code Of Conduct](#code-of-conduct) 56 | - [License](#license) 57 | 58 | 59 | 60 | 61 | 62 |
63 | 64 | ## Installation 65 | 66 | ``` 67 | yarn add -D @hirez_io/jasmine-single 68 | ``` 69 | 70 | or 71 | 72 | ``` 73 | npm install -D @hirez_io/jasmine-single 74 | ``` 75 | 76 | ### Using TypeScript? 77 | 78 |
79 | CLICK HERE TO EXPAND 80 | 81 |
82 | 83 | You should add `@hirez_io/jasmine-single` to your `types` property under `compilerOptions` in your `tsconfig.json` (or `tsconfig.spec.json`) like this: 84 | 85 | ```js 86 | // tsconfig.json or tsconfig.spec.json 87 | 88 | { 89 | ... 90 | "compilerOptions": { 91 | "types": [ 92 | "jasmine", 93 | "@hirez_io/jasmine-single", // 👈 ADD THIS 94 | 95 | // ...any other types you might have... 96 | ], 97 | ... 98 | } 99 | ... 100 | } 101 | ``` 102 | 103 | ⚠ **ATTENTION:** If you have `typeRoots` configured like this - 104 | 105 | ```ts 106 | "compilerOptions": { 107 | "typeRoots": [ 108 | "node_modules/@types" 109 | ], 110 | } 111 | ``` 112 | 113 | You should add `"node_modules"` like this - 114 | 115 | ```ts 116 | "compilerOptions": { 117 | "typeRoots": [ 118 | "node_modules/@types", 119 | "node_modules/@hirez_io" // 👈 ADD THIS 120 | ], 121 | } 122 | ``` 123 | 124 | or else it won't find `@hirez_io/jasmine-single` global types. 125 | ### ⚠ **VS CODE USERS:** 126 | 127 | Add the above configuration (`types` and/or `typeRoots`) to your `tsconfig.json` specifically or else it would not recognize the global types. 128 | 129 |
130 | 131 |
132 | 133 | ### Using karma? 134 | 135 |
136 | CLICK HERE TO EXPAND 137 | 138 |
139 | 140 | `@hirez_io/jasmine-single` has a dependency on `@hirez_io/karma-jasmine-single` which is a karma plugin (inspired by [karma-jasmine-given](https://github.com/kirisu/karma-jasmine-given)) I rewrote to save you the hassle of loading the library script yourself. 141 | 142 | So it will automatically installs `@hirez_io/karma-jasmine-single` for you 😎 143 | 144 | Here's how to modify your `karma.conf.js`: 145 | 146 | ```js 147 | // karma.conf.js 148 | 149 | module.exports = function(config) { 150 | config.set({ 151 | 152 | plugins: [ 153 | require('karma-jasmine'), 154 | require('@hirez_io/karma-jasmine-single'), // 👈 ADD THIS 155 | require('karma-chrome-launcher') 156 | // other plugins you might have... 157 | ], 158 | 159 | frameworks: [ 160 | '@hirez_io/jasmine-single', // 👈 ADD THIS 161 | 'jasmine', 162 | // other frameworks... 163 | ], 164 | 165 | // ... 166 | ``` 167 | 168 | 169 |
170 | 171 |
172 | 173 | 174 | ## What are "single-action" tests? 175 | 176 | A single-action test is a test with only one action. (CAPTAIN OBVIOUS! 🦸‍♂️😅) 177 | 178 | Normally, you setup the environment, you call an action and then you check the output. 179 | 180 | #### What's an action? 181 | 182 | Well... it can be a method call, a button click or anything else our test is checking. 183 | 184 | The big idea here is that it should be only **ONE ACTION PER TEST**. 185 | 186 |
187 | 188 | ## Why writing single-action tests is good? 189 | 190 | Single action tests are more "Test Effective" compared to multi-action tests. 191 | 192 | The benefits of single-action tests: 193 | 194 | ✅ Your tests will **break less often** (making them more effective) 195 | 196 | ✅ Whenever something breaks, you have **only one "action" code to debug** 197 | 198 | ✅ They promote **better coverage** (easier to see which cases are still missing) 199 | 200 | ✅ They give you **better structure** (every part of your test has a clear goal) 201 | 202 | 203 |
204 | 205 | ## How to write single-action tests? 206 | 207 | This library follows the natural "`given`, `when` and `then`" structure (some of you know it as "Arrange, Act, Assert"). 208 | 209 | This means every test has only 3 parts to it, no more. 210 | 211 | ```ts 212 | describe('addTwo', () => { 213 | 214 | // This is where you setup your environment / inputs 215 | given('first number is 1', () => { 216 | const firstNum = 1; 217 | 218 | // This is where you call the action under test 219 | when('adding 2 to the first number', () => { 220 | const actualResult = addTwo(firstNum); 221 | 222 | // This is where you check the outcome 223 | then('result should be 3', () => { 224 | expect(actualResult).toEqual(3); 225 | }); 226 | }); 227 | }); 228 | 229 | }); 230 | 231 | ``` 232 | 233 | It also prints a nicer test description when there's an error: 234 | 235 | ``` 236 | CONSOLE OUTPUT: 237 | ~~~~~~~~~~~~~~ 238 | 239 | GIVEN first number is 1 240 | WHEN adding 2 to the first number 241 | THEN result should be 3 242 | ``` 243 | 244 |
245 | 246 | ## What's wrong with using `it()` for single-action tests? 247 | 248 | Did you know that the most common way of writing JavaScript tests dates back to 2005? 😅 249 | 250 | Jasmine, which was created in 2009 was inspired by Ruby's testing framework - RSpec which was created in 2005. 251 | 252 | 253 | Originally, RSpec introduced the syntax of "`describe` > `context` > `it`", where `context` was meant to be used as the "setup" part of the test. 254 | 255 | Unfortunately, the `context` wasn't ported to Jasmine so we got used to writing our tests in the "`describe` > `it`" structure... which is more limited. 256 | 257 |
258 | 259 | _Here are a couple of limitations with the common `it()` structure:_ 260 | 261 | ### ❌ 1. It promotes partial or awkward descriptions of tests 262 | 263 | The word "it" kinda forces you to begin the description with "should" which leads to focusing specifically on just the "outcome" part of the test (the `then`). 264 | 265 | But if you want to add more context (like what should be the input that causes that outcome) things start to get messy. 266 | 267 | Because there isn't a clear convention, people tend to invent their own on the fly which creates inconsistency. 268 | 269 | 270 | **Example:** 271 | ```ts 272 | it('should do X only when environment is Y and also called by Z But only if...you get the point', ()=> ...) 273 | ``` 274 | 275 |
276 | 277 | ### ❌ 2. Nothing prevents you from writing multi-action tests 278 | 279 | This mixes up testing structures and making them harder to understand 280 | 281 | **Example:** 282 | ```ts 283 | it('should transform the products', ()=> { 284 | 285 | // SETUP 286 | const fakeProducts = [...]; 287 | 288 | // ACTION 289 | const result = classUnderTest.transformProducts(fakeProducts); 290 | 291 | // OUTCOME 292 | const transformedProducts = [...]; 293 | expect(result).toEqual(transformedProducts); 294 | 295 | // ACTION 296 | const result2 = classUnderTest.transformProducts(); 297 | 298 | // OUTCOME 299 | expect(result2).toEqual( [] ); 300 | 301 | 302 | // this 👆 is a multi-action test. 303 | 304 | }) 305 | ``` 306 |
307 | 308 | ### ❌ 3. Detailed descriptions can get out of date more easily 309 | 310 | The farther the description is from the actual implementation the less likely you'll remember to update it when the test code changes 311 | 312 | **Example:** 313 | ```ts 314 | test('GIVEN valid products and metadata returned successfully WHEN destroying the products THEN they should get decorated', ()=> { 315 | 316 | const fakeProducts = [...]; 317 | const fakeMetadata = [...]; 318 | mySpy.getMetadata.and.returnValue(fakeMetadata); 319 | 320 | const result = classUnderTest.transformProducts(fakeProducts); 321 | 322 | const decoratedProducts = [...]; 323 | expect(result).toEqual(decoratedProducts); 324 | 325 | }) 326 | ``` 327 |
328 | 329 | #### Did you spot the typo? 👆😅 330 | 331 | 332 | (it should be _"transforming"_ instead of _"destroying"_) 333 | 334 |
335 | 336 | Compare that to - 337 | 338 | ```ts 339 | 340 | given('valid products and metadata returned successfully', () => { 341 | const fakeProducts = [...]; 342 | const fakeMetadata = [...]; 343 | mySpy.getMetadata.and.returnValue(fakeMetadata); 344 | 345 | // 👇 --> easier to spot as it's closer to the implementation 346 | when('destroying the products', () => { 347 | const result = classUnderTest.transformProducts(fakeProducts); 348 | 349 | then('they should get decorated', () => { 350 | const decoratedProducts = [...]; 351 | expect(result).toEqual(decoratedProducts); 352 | }); 353 | }); 354 | }); 355 | 356 | 357 | ``` 358 | 359 |
360 | 361 | 362 | 363 | ## Usage 364 | 365 | ### ▶ The basic testing structure 366 | 367 | The basic structure is a nesting of these 3 functions: 368 | 369 | ```ts 370 | given(description, () => { 371 | when(description, () => { 372 | then(description, () => { 373 | 374 | }) 375 | }) 376 | }) 377 | 378 | 379 | ``` 380 | 381 | **EXAMPLE:** 382 | ```ts 383 | describe('addTwo', () => { 384 | 385 | // This is where you setup your environment / inputs 386 | given('first number is 1', () => { 387 | const firstNum = 1; 388 | 389 | // This is where you call the action under test 390 | when('adding 2 to the first number', () => { 391 | const actualResult = addTwo(firstNum); 392 | 393 | // This is where you check the outcome 394 | then('result should be 3', () => { 395 | expect(actualResult).toEqual(3); 396 | }); 397 | }); 398 | }); 399 | 400 | }); 401 | 402 | ``` 403 | 404 | Under the hood it creates a regular `it()` test with a combination of all the descriptions: 405 | 406 | ``` 407 | CONSOLE OUTPUT: 408 | ~~~~~~~~~~~~~~ 409 | 410 | GIVEN first number is 1 411 | WHEN adding 2 to the first number 412 | THEN result should be 3 413 | ``` 414 | 415 |
416 | 417 | ### ▶ Meaningful error messages 418 | 419 | This library will throw an error if you deviate from the `given > when > then` structure. 420 | 421 | So you won't be tempted to accidentally turn your single-action test into a multi-action one. 422 | 423 | ```ts 424 | describe('addTwo', () => { 425 | 426 | // 👉 ATTENTION: You cannot start with a "when()" or a "then()" 427 | // the test MUST start with a "given()" 428 | 429 | 430 | given('first number is 1', () => { 431 | const firstNum = 1; 432 | 433 | // 👉 ATTENTION: You cannot add here a "then()" function directly 434 | // or another "given()" function 435 | 436 | when('adding 2 to the first number', () => { 437 | const actualResult = addTwo(firstNum); 438 | 439 | // 👉 ATTENTION: You cannot add here a "given()" function 440 | // or another "when()" function 441 | 442 | then('result should be 3', () => { 443 | expect(actualResult).toEqual(3); 444 | 445 | 446 | // 👉 ATTENTION: You cannot add here a "given()" function 447 | // or a "when()" function or another "then()" 448 | }); 449 | }); 450 | }); 451 | 452 | }); 453 | 454 | ``` 455 | 456 | 457 | ### ▶ `async` / `await` support 458 | 459 | **Example:** 460 | 461 | ```ts 462 | describe('addTwo', () => { 463 | 464 | given('first number is 1', () => { 465 | const firstNum = 1; 466 | // 👇 467 | when('adding 2 to the first number', async () => { 468 | const actualResult = await addTwo(firstNum); 469 | 470 | then('result should be 3', () => { 471 | expect(actualResult).toEqual(3); 472 | }); 473 | }); 474 | }); 475 | 476 | }); 477 | 478 | 479 | ``` 480 | 481 | ### ▶ `done()` function support 482 | 483 | The `given` function supports the (old) async callback way of using a `done()` function to signal when the test is completed. 484 | 485 | ```ts 486 | describe('addTwo', () => { 487 | // 👇 488 | given('first number is 1', (done) => { 489 | const firstNum = 1; 490 | 491 | when('adding 2 to the first number', () => { 492 | const actualResult = addTwo(firstNum, function callback() { 493 | 494 | then('result should be 3', () => { 495 | expect(actualResult).toEqual(3); 496 | done(); 497 | }); 498 | 499 | }); 500 | }); 501 | 502 | }); 503 | 504 | }); 505 | 506 | 507 | ``` 508 | 509 | ℹ It also supports `done(error)` or `done.fail(error)` for throwing async errors. 510 | 511 |
512 | 513 | ## Contributing 514 | 515 | Want to contribute? Yayy! 🎉 516 | 517 | Please read and follow our [Contributing Guidelines](../../CONTRIBUTING.md) to learn what are the right steps to take before contributing your time, effort and code. 518 | 519 | Thanks 🙏 520 | 521 |
522 | 523 | ## Code Of Conduct 524 | 525 | Be kind to each other and please read our [code of conduct](../../CODE_OF_CONDUCT.md). 526 | 527 | 528 |
529 | 530 | ## Contributors ✨ 531 | 532 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 |

Shai Reznik

💻 📖 🤔 🚇 🚧 🧑‍🏫 👀 ⚠️

Maarten Tibau

📖 🚇

Benjamin Gruenbaum

💻 🤔 🚧
544 | 545 | 546 | 547 | 548 | 549 | 550 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 551 | 552 |
553 | 554 | ## License 555 | 556 | MIT 557 | -------------------------------------------------------------------------------- /packages/jasmine-single/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "packages/jasmine-single/spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.ts" 5 | ], 6 | "helpers": [], 7 | "stopSpecOnExpectationFailure": true, 8 | "stopOnSpecFailure": true, 9 | "random": true 10 | } 11 | -------------------------------------------------------------------------------- /packages/jasmine-single/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hirez_io/jasmine-single", 3 | "version": "1.1.6", 4 | "publishConfig": { 5 | "access": "public", 6 | "registry": "https://registry.npmjs.org" 7 | }, 8 | "author": { 9 | "name": "Shai Reznik", 10 | "company": "HiRez.io" 11 | }, 12 | "description": "A Jasmine addon that helps you write 'Single-Action Tests' by breaking them into a given / when / then structure.", 13 | "keywords": [ 14 | "jasmine", 15 | "jasmine-single", 16 | "gwt", 17 | "Given When Then", 18 | "Microtests", 19 | "Single Action Tests", 20 | "Angular Tests", 21 | "Testing", 22 | "Unit tests", 23 | "JavasScript Unit Tests", 24 | "TypeScript Unit Tests", 25 | "hirez.io" 26 | ], 27 | "homepage": "https://github.com/hirezio/single/tree/main/packages/jasmine-single", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/hirezio/single", 31 | "directory": "packages/jasmine-single" 32 | }, 33 | "license": "MIT", 34 | "source": "temp-src/jasmine-single.ts", 35 | "main": "dist/jasmine-single.js", 36 | "types": "dist/jasmine-single.d.ts", 37 | "scripts": { 38 | "clean": "rimraf dist", 39 | "clean:temp-src": "rimraf temp-src", 40 | "copy:shared-source": "cpy ../../shared/single-core/single-core.ts ./temp-src/ --rename=jasmine-single.ts", 41 | "compile": "microbundle -f iife --tsconfig tsconfig.build.json --compress false", 42 | "build": "run-s clean copy:shared-source compile clean:temp-src", 43 | "test": "echo \n*** Run tests from the root folder\n" 44 | }, 45 | "dependencies": { 46 | "@hirez_io/karma-jasmine-single": "^1.0.4" 47 | }, 48 | "peerDependencies": { 49 | "jasmine-core": "< 5.x" 50 | }, 51 | "devDependencies": { 52 | "@types/jasmine": "3.10.3", 53 | "cpy-cli": "^3.1.1", 54 | "jasmine": "4.0.2", 55 | "microbundle": "^0.15.0", 56 | "nodemon": "2.0.16", 57 | "nyc": "15.1.0", 58 | "source-map-support": "^0.5.21", 59 | "tsconfig-paths": "3.10.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/jasmine-single/spec/jasmine-single.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getOnlyOneError, 3 | getMustBeAChildError, 4 | } from '../../../shared/single-core/single-core'; 5 | 6 | const root = (1, eval)('this'); 7 | 8 | describe('Jasmine Single', () => { 9 | function addTwo(num: number | undefined) { 10 | if (num) { 11 | return num + 2; 12 | } 13 | return undefined; 14 | } 15 | 16 | function addTwoAsync(num: number | undefined): Promise { 17 | if (num) { 18 | return Promise.resolve(num + 2); 19 | } 20 | return Promise.resolve(); 21 | } 22 | 23 | describe('A synchronous test', () => { 24 | given('input is set to 1', () => { 25 | const fakeNumber = 1; 26 | 27 | when(`adding 2`, () => { 28 | const actualResult = addTwo(fakeNumber); 29 | 30 | then('result should be 3', () => { 31 | expect(actualResult).toBe(3); 32 | }); 33 | }); 34 | }); 35 | 36 | given('input is set to 1', () => { 37 | const fakeNumber = 1; 38 | 39 | when('adding 2', () => { 40 | const actualResult = addTwo(fakeNumber); 41 | 42 | then('result should be 3', () => { 43 | expect(actualResult).toBe(3); 44 | }); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('full description', () => { 50 | it('should combine all 3 descriptions', () => { 51 | const itSpy = spyOn(root, 'it'); 52 | given('fake given description', () => { 53 | when(`fake when description`, () => { 54 | then('fake then description', () => {}); 55 | }); 56 | }); 57 | const expectedFullDescription = ` 58 | GIVEN fake given description 59 | WHEN fake when description 60 | THEN fake then description`; 61 | 62 | const actualDescription = itSpy.calls.first().args[0]; 63 | expect(actualDescription).toEqual(expectedFullDescription); 64 | }); 65 | }); 66 | 67 | describe('focus and exclude', () => { 68 | it('should be able to focus on 1 given using "fgiven"', () => { 69 | const fitSpy = spyOn(root, 'fit'); 70 | fgiven('FAKE DESCRIPTION', function () {}); 71 | 72 | expect(fitSpy).toHaveBeenCalled(); 73 | }); 74 | 75 | it('should be able to focus on 1 given using "given.only"', () => { 76 | const fitSpy = spyOn(root, 'fit'); 77 | given.only('FAKE DESCRIPTION', function () {}); 78 | 79 | expect(fitSpy).toHaveBeenCalled(); 80 | }); 81 | 82 | it('should not allow "fgiven" not as a root given', () => { 83 | spyOn(root, 'it').and.callFake((desc: string, fn: Function) => fn()); 84 | 85 | given('root given', () => { 86 | try { 87 | fgiven('another given', () => {}); 88 | } catch (error: any) { 89 | expect(error.message).toEqual(getOnlyOneError('given')); 90 | } 91 | }); 92 | }); 93 | 94 | it('should combine all 3 descriptions for fgiven as well', () => { 95 | 96 | const itSpy = spyOn(root, 'fit'); 97 | 98 | fgiven('fake given description', () => { 99 | when(`fake when description`, () => { 100 | then('fake then description', () => {}); 101 | }); 102 | }); 103 | const expectedFullDescription = ` 104 | GIVEN fake given description 105 | WHEN fake when description 106 | THEN fake then description`; 107 | 108 | const actualDescription = itSpy.calls.first().args[0]; 109 | expect(actualDescription).toEqual(expectedFullDescription); 110 | }); 111 | 112 | 113 | it('should be able to exclude a given using "xgiven"', () => { 114 | const xitSpy = spyOn(root, 'xit'); 115 | xgiven('FAKE DESCRIPTION', function () {}); 116 | 117 | expect(xitSpy).toHaveBeenCalled(); 118 | }); 119 | 120 | it('should be able to exclude a given using "given.skip"', () => { 121 | const xitSpy = spyOn(root, 'xit'); 122 | given.skip('FAKE DESCRIPTION', function () {}); 123 | 124 | expect(xitSpy).toHaveBeenCalled(); 125 | }); 126 | }); 127 | 128 | describe('Async tests', () => { 129 | 130 | given('a "done" function gets passed', (done) => { 131 | const fakeNumber = 1; 132 | 133 | when('adding 2 asynchronously', () => { 134 | const actualResult = addTwo(fakeNumber); 135 | 136 | setTimeout(() => { 137 | then('result should be 3', () => { 138 | expect(actualResult).toBe(3); 139 | done(); 140 | }); 141 | }, 0); 142 | 143 | }); 144 | }); 145 | 146 | it('should handle errors passed to "done"', async () => { 147 | let actualPromiseFromGiven: Promise | undefined = undefined; 148 | spyOn(root, 'it').and.callFake((desc: any, fn: any) => { 149 | actualPromiseFromGiven = fn(); 150 | }); 151 | 152 | given('given', (done) => { 153 | when('when', () => { 154 | then('then', () => { 155 | done(new Error('FAKE ERROR')); 156 | }); 157 | }); 158 | }); 159 | try { 160 | await actualPromiseFromGiven; 161 | 162 | } catch (error: any) { 163 | expect(error.message).toEqual('FAKE ERROR'); 164 | } 165 | }); 166 | 167 | it('should handle errors passed to "done.fail()"', async () => { 168 | let actualPromiseFromGiven: Promise | undefined = undefined; 169 | spyOn(root, 'it').and.callFake((desc: any, fn: any) => { 170 | actualPromiseFromGiven = fn(); 171 | }); 172 | 173 | given('given', (done) => { 174 | when('when', () => { 175 | then('then', () => { 176 | done.fail(new Error('FAKE ERROR')); 177 | }); 178 | }); 179 | }); 180 | try { 181 | await actualPromiseFromGiven; 182 | 183 | } catch (error: any) { 184 | expect(error.message).toEqual('FAKE ERROR'); 185 | } 186 | }); 187 | 188 | it('should handle promise errors thrown inside "when"', async () => { 189 | let actualPromiseFromGiven: Promise | undefined = undefined; 190 | spyOn(root, 'it').and.callFake((desc: any, fn: any) => { 191 | actualPromiseFromGiven = fn(); 192 | }); 193 | 194 | given('given', async () => { 195 | when('when', async () => { 196 | await Promise.resolve().then(() => { 197 | throw new Error('FAKE ERROR'); 198 | }) 199 | 200 | }) 201 | }); 202 | try { 203 | await actualPromiseFromGiven; 204 | 205 | } catch (error: any) { 206 | expect(error.message).toEqual('FAKE ERROR'); 207 | } 208 | }); 209 | 210 | it('should handle promise errors thrown inside "then"', async () => { 211 | let actualPromiseFromGiven: Promise | undefined = undefined; 212 | spyOn(root, 'it').and.callFake((desc: any, fn: any) => { 213 | actualPromiseFromGiven = fn(); 214 | }); 215 | 216 | given('given', async () => { 217 | when('when', async () => { 218 | then('then', async () => { 219 | await Promise.resolve().then(() => { 220 | throw new Error('FAKE ERROR'); 221 | }) 222 | 223 | }) 224 | }) 225 | }); 226 | try { 227 | await actualPromiseFromGiven; 228 | 229 | } catch (error: any) { 230 | expect(error.message).toEqual('FAKE ERROR'); 231 | } 232 | }); 233 | 234 | 235 | given('input is set to 1', () => { 236 | const fakeNumber = 1; 237 | 238 | when('adding 2 asynchronously', async () => { 239 | const actualResult = await addTwoAsync(fakeNumber); 240 | const actualResult2 = await addTwoAsync(fakeNumber); 241 | 242 | then('result should be 3', () => { 243 | expect(actualResult).toBe(3); 244 | expect(actualResult2).toBe(3); 245 | }); 246 | }); 247 | }); 248 | 249 | given('input is set to async 1', async () => { 250 | const fakeNumber = await 1; 251 | 252 | when('adding 2 asynchronously', async () => { 253 | const actualResult = await addTwoAsync(fakeNumber); 254 | 255 | then('async result should be 3', async () => { 256 | const expectedResult = await Promise.resolve(3); 257 | expect(actualResult).toBe(expectedResult); 258 | }); 259 | }); 260 | }); 261 | }); 262 | 263 | describe('wrong order of functions', () => { 264 | it('should not allow "given" inside of another "given"', () => { 265 | spyOn(root, 'it').and.callFake((desc: string, fn: Function) => fn()); 266 | 267 | given('root given', () => { 268 | try { 269 | given('another given', () => {}); 270 | } catch (error: any) { 271 | expect(error.message).toEqual(getOnlyOneError('given')); 272 | } 273 | }); 274 | }); 275 | 276 | it('should not allow "given" inside of another "when"', () => { 277 | spyOn(root, 'it').and.callFake((desc: string, fn: Function) => fn()); 278 | 279 | given('root given', () => { 280 | when('fake when', () => { 281 | try { 282 | given('another given', () => {}); 283 | } catch (error: any) { 284 | expect(error.message).toEqual(getOnlyOneError('given')); 285 | } 286 | }); 287 | }); 288 | }); 289 | 290 | it('should not allow "given" inside of another "then"', () => { 291 | spyOn(root, 'it').and.callFake((desc: string, fn: Function) => fn()); 292 | 293 | given('root given', () => { 294 | when('fake when', () => { 295 | then('fake then', () => { 296 | try { 297 | given('another given', () => {}); 298 | } catch (error: any) { 299 | expect(error.message).toEqual(getOnlyOneError('given')); 300 | } 301 | }); 302 | }); 303 | }); 304 | }); 305 | 306 | it('should not allow "when" without a "given" parent', () => { 307 | try { 308 | when('fake when', () => {}); 309 | fail('The function "when" should have thrown an error'); 310 | } catch (error: any) { 311 | expect(error.message).toEqual(getMustBeAChildError('when')); 312 | } 313 | }); 314 | 315 | it('should not allow "then" without a "when" parent', () => { 316 | try { 317 | then('fake then', () => {}); 318 | fail('The function "then" should have thrown an error'); 319 | } catch (error: any) { 320 | expect(error.message).toEqual(getMustBeAChildError('then')); 321 | } 322 | }); 323 | it('should not allow "then" straight under a "given"', () => { 324 | spyOn(root, 'it').and.callFake((desc: string, fn: Function) => fn()); 325 | given('root given', () => { 326 | try { 327 | then('fake then', () => {}); 328 | fail('The function "then" should have thrown an error'); 329 | } catch (error: any) { 330 | expect(error.message).toEqual(getMustBeAChildError('then')); 331 | } 332 | }); 333 | }); 334 | 335 | it('should not allow 2 "when" functions under 1 "given"', () => { 336 | spyOn(root, 'it').and.callFake((desc: string, fn: Function) => fn()); 337 | given('root given', () => { 338 | try { 339 | when('first when', () => {}); 340 | when('second when', () => {}); 341 | fail('The second "when" function should have thrown an error'); 342 | } catch (error: any) { 343 | expect(error.message).toEqual(getOnlyOneError('when')); 344 | } 345 | }); 346 | }); 347 | 348 | it('should not allow 2 "then" functions under 1 "when"', () => { 349 | spyOn(root, 'it').and.callFake((desc: string, fn: Function) => fn()); 350 | given('root given', () => { 351 | when('when', () => { 352 | try { 353 | then('first then', () => {}); 354 | then('second then', () => {}); 355 | fail('The second "then" function should have thrown an error'); 356 | } catch (error: any) { 357 | expect(error.message).toEqual(getOnlyOneError('then')); 358 | } 359 | }); 360 | }); 361 | }); 362 | }); 363 | }); 364 | -------------------------------------------------------------------------------- /packages/jasmine-single/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "rootDir": "./temp-src" 6 | }, 7 | "include": [ 8 | "temp-src" 9 | ], 10 | "exclude": ["**/*.spec.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/jasmine-single/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "declaration": true, // generate .d.ts files 9 | "declarationMap": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": false, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "sourceMap": true, 15 | "downlevelIteration": true, 16 | "lib": ["es2015", "es2016", "dom"], 17 | "pretty": true, 18 | "strict": true, 19 | "baseUrl": ".", 20 | "outDir": "dist", 21 | "types": ["jasmine", "node"] 22 | }, 23 | "include": ["spec"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/jest-single/.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "single", 3 | "projectOwner": "hirezio", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md", 8 | "../../README.md" 9 | ], 10 | "imageSize": 100, 11 | "commit": true, 12 | "commitConvention": "angular", 13 | "contributors": [ 14 | { 15 | "login": "shairez", 16 | "name": "Shai Reznik", 17 | "avatar_url": "https://avatars1.githubusercontent.com/u/1430726?v=4", 18 | "profile": "https://www.hirez.io/?utm_medium=Open_Source&utm_source=Github&utm_campaign=Lead_Generation&utm_content=jest-single--all-contributors-profile-link", 19 | "contributions": [ 20 | "code", 21 | "doc", 22 | "ideas", 23 | "infra", 24 | "maintenance", 25 | "mentoring", 26 | "review", 27 | "test" 28 | ] 29 | }, 30 | { 31 | "login": "maartentibau", 32 | "name": "Maarten Tibau", 33 | "avatar_url": "https://avatars.githubusercontent.com/u/4103756?v=4", 34 | "profile": "https://www.webtrix.be", 35 | "contributions": [ 36 | "doc", 37 | "infra" 38 | ] 39 | }, 40 | { 41 | "login": "benjamingr", 42 | "name": "Benjamin Gruenbaum", 43 | "avatar_url": "https://avatars.githubusercontent.com/u/1315533?v=4", 44 | "profile": "https://stackoverflow.com/users/1348195/benjamin-gruenbaum", 45 | "contributions": [ 46 | "code", 47 | "ideas", 48 | "maintenance" 49 | ] 50 | } 51 | ], 52 | "contributorsPerLine": 7 53 | } 54 | -------------------------------------------------------------------------------- /packages/jest-single/.core-version: -------------------------------------------------------------------------------- 1 | 3f24aa9551ab834f07b3464b2043721fb3b55edf -------------------------------------------------------------------------------- /packages/jest-single/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage -------------------------------------------------------------------------------- /packages/jest-single/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.1.8](https://github.com/hirezio/single/compare/@hirez_io/jest-single@1.1.7...@hirez_io/jest-single@1.1.8) (2023-08-25) 7 | 8 | **Note:** Version bump only for package @hirez_io/jest-single 9 | 10 | 11 | 12 | 13 | 14 | ## [1.1.7](https://github.com/hirezio/single/compare/@hirez_io/jest-single@1.1.6...@hirez_io/jest-single@1.1.7) (2023-08-25) 15 | 16 | **Note:** Version bump only for package @hirez_io/jest-single 17 | 18 | 19 | 20 | 21 | 22 | ## [1.1.6](https://github.com/hirezio/single/compare/@hirez_io/jest-single@1.1.5...@hirez_io/jest-single@1.1.6) (2022-07-11) 23 | 24 | **Note:** Version bump only for package @hirez_io/jest-single 25 | 26 | 27 | 28 | 29 | 30 | ## [1.1.5](https://github.com/hirezio/single/compare/@hirez_io/jest-single@1.1.4...@hirez_io/jest-single@1.1.5) (2022-06-04) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * **global:** fix fgiven description ([dc75bd9](https://github.com/hirezio/single/commit/dc75bd9ff24b6f4f5b50ed734e1ffed99b9c46ae)) 36 | * **global:** fix unhandled promise exceptions bug ([06f4934](https://github.com/hirezio/single/commit/06f4934c607aeec06520874a2563e2bd002a1337)) 37 | 38 | 39 | 40 | 41 | 42 | ## [1.1.4](https://github.com/hirezio/single/compare/@hirez_io/jest-single@1.1.3...@hirez_io/jest-single@1.1.4) (2022-03-13) 43 | 44 | **Note:** Version bump only for package @hirez_io/jest-single 45 | 46 | 47 | 48 | 49 | 50 | ## [1.1.3](https://github.com/hirezio/single/compare/@hirez_io/jest-single@1.1.2...@hirez_io/jest-single@1.1.3) (2022-03-13) 51 | 52 | **Note:** Version bump only for package @hirez_io/jest-single 53 | 54 | 55 | 56 | 57 | 58 | ## [1.1.2](https://github.com/hirezio/single/compare/@hirez_io/jest-single@1.1.1...@hirez_io/jest-single@1.1.2) (2022-03-02) 59 | 60 | **Note:** Version bump only for package @hirez_io/jest-single 61 | 62 | 63 | 64 | 65 | 66 | ## [1.1.1](https://github.com/hirezio/single/compare/@hirez_io/jest-single@1.1.0...@hirez_io/jest-single@1.1.1) (2021-08-08) 67 | 68 | **Note:** Version bump only for package @hirez_io/jest-single 69 | 70 | 71 | 72 | 73 | 74 | # [1.1.0](https://github.com/hirezio/single/compare/@hirez_io/jest-single@1.0.0...@hirez_io/jest-single@1.1.0) (2021-08-08) 75 | 76 | 77 | ### Features 78 | 79 | * **global:** add support for `done` and update README ([6082c27](https://github.com/hirezio/single/commit/6082c2710153ea0a5288a25457a7a78828a7b48d)) 80 | 81 | 82 | 83 | 84 | 85 | # 1.0.0 (2021-07-29) 86 | 87 | **Note:** Version bump only for package @hirez_io/jest-single 88 | -------------------------------------------------------------------------------- /packages/jest-single/README.md: -------------------------------------------------------------------------------- 1 | # `@hirez_io/jest-single` 📃👌 2 | 3 | A Jest addon that helps you write 'Single-Action Tests' by breaking them into a **"given / when / then"** structure. 4 | 5 | [![npm version](https://img.shields.io/npm/v/@hirez_io/jest-single.svg?style=flat-square)](https://www.npmjs.org/package/@hirez_io/jest-single) 6 | [![npm downloads](https://img.shields.io/npm/dm/@hirez_io/jest-single.svg?style=flat-square)](http://npm-stat.com/charts.html?package=@hirez_io/jest-single&from=2017-07-26) 7 | [![codecov](https://img.shields.io/codecov/c/github/hirezio/single.svg)](https://codecov.io/gh/hirezio/single) 8 | ![Build and optionally publish](https://github.com/hirezio/single/workflows/Build%20and%20optionally%20publish/badge.svg) 9 | [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) 10 | [![Code of Conduct](https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square)](../../CODE_OF_CONDUCT.md) 11 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 12 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-green.svg?style=flat-square)](#contributors-) 13 | 14 | 15 |
16 | 17 | TestAngular.com - Free Angular Testing Workshop - The Roadmap to Angular Testing Mastery 21 | 22 |
23 | 24 |
25 | 26 | # Table of Contents 27 | 28 | 29 | 30 | 31 | - [Installation](#installation) 32 | 33 | - [Configuring Jest](#configuring-jest) 34 | 35 | - [Using TypeScript?](#using-typescript) 36 | 37 | - [What are "single-action" tests?](#what-are-single-action-tests) 38 | 39 | - [Why writing single-action tests is good?](#why-writing-single-action-tests-is-good) 40 | 41 | - [How to write single-action tests?](#how-to-write-single-action-tests) 42 | 43 | - [What's wrong with using `it()` or `test()` for single-action tests?](#whats-wrong-with-using-it-or-test-for-single-action-tests) 44 | 45 | - [Usage](#usage) 46 | 47 | - [▶ The basic testing structure](#%E2%96%B6-the-basic-testing-structure) 48 | 49 | - [▶ Meaningful error messages](#%E2%96%B6-meaningful-error-messages) 50 | 51 | - [▶ `async` / `await` support](#%E2%96%B6-async--await-support) 52 | 53 | - [▶ `done()` function support](#%E2%96%B6-done-function-support) 54 | - [Contributing](#contributing) 55 | - [Code Of Conduct](#code-of-conduct) 56 | - [License](#license) 57 | 58 | 59 | 60 | 61 | 62 |
63 | 64 | 65 | ## Installation 66 | 67 | ``` 68 | yarn add -D @hirez_io/jest-single 69 | ``` 70 | 71 | or 72 | 73 | ``` 74 | npm install -D @hirez_io/jest-single 75 | ``` 76 | 77 | ## Configuring Jest 78 | 79 | Add the following line to your jest config: 80 | 81 | ```json 82 | "setupFilesAfterEnv": ["node_modules/@hirez_io/jest-single/dist/jest-single.js"] 83 | ``` 84 | 85 | ⚠ **ATTENTION:** If you have configured `rootDir` - 86 | 87 | Make sure you have the right path to `node_modules`. 88 | 89 | For example: 90 | 91 | ```json 92 | "rootDir": "src", 93 | "setupFilesAfterEnv": ["../node_modules/@hirez_io/jest-single/dist/jest-single.js"] 94 | ``` 95 | 96 | 97 | 98 |
99 | 100 | 101 | ## Using TypeScript? 102 | 103 |
104 | CLICK HERE TO EXPAND 105 | 106 |
107 | 108 | You should add `@hirez_io/jest-single` to your `types` property under `compilerOptions` in your `tsconfig.json` (or `tsconfig.spec.json`) like this: 109 | 110 | ```js 111 | // tsconfig.json or tsconfig.spec.json 112 | 113 | { 114 | ... 115 | "compilerOptions": { 116 | "types": [ 117 | "jest", 118 | "@hirez_io/jest-single", // 👈 ADD THIS 119 | 120 | // ...any other types you might have... 121 | ], 122 | ... 123 | } 124 | ... 125 | } 126 | ``` 127 | 128 | ⚠ **ATTENTION:** If you have `typeRoots` configured like this - 129 | 130 | ```ts 131 | "compilerOptions": { 132 | "typeRoots": [ 133 | "node_modules/@types" 134 | ], 135 | } 136 | ``` 137 | 138 | You should add `"node_modules"` like this - 139 | 140 | ```ts 141 | "compilerOptions": { 142 | "typeRoots": [ 143 | "node_modules/@types", 144 | "node_modules/@hirez_io" // 👈 ADD THIS 145 | ], 146 | } 147 | ``` 148 | 149 | or else it won't find `@hirez_io/jest-single` global types. 150 | 151 | ### ⚠ **VS CODE USERS:** 152 | 153 | Add the above configuration (`types` and/or `typeRoots`) to your `tsconfig.json` specifically or else it would not recognize the global types. 154 | 155 |
156 | 157 |
158 | 159 | ## What are "single-action" tests? 160 | 161 | A single-action test is a test with only one action. (CAPTAIN OBVIOUS! 🦸‍♂️😅) 162 | 163 | Normally, you setup the environment, you call an action and then you check the output. 164 | 165 | #### What's an action? 166 | 167 | Well... it can be a method call, a button click or anything else our test is checking. 168 | 169 | The big idea here is that it should be only **ONE ACTION PER TEST**. 170 | 171 |
172 | 173 | ## Why writing single-action tests is good? 174 | 175 | Single action tests are more "Test Effective" compared to multi-action tests. 176 | 177 | The benefits of single-action tests: 178 | 179 | ✅ Your tests will **break less often** (making them more effective) 180 | 181 | ✅ Whenever something breaks, you have **only one "action" code to debug** 182 | 183 | ✅ They promote **better coverage** (easier to see which cases are still missing) 184 | 185 | ✅ They give you **better structure** (every part of your test has a clear goal) 186 | 187 | 188 |
189 | 190 | ## How to write single-action tests? 191 | 192 | This library follows the natural "`given`, `when` and `then`" structure (some of you know it as "Arrange, Act, Assert"). 193 | 194 | This means every test has only 3 parts to it, no more. 195 | 196 | ```ts 197 | describe('addTwo', () => { 198 | 199 | // This is where you setup your environment / inputs 200 | given('first number is 1', () => { 201 | const firstNum = 1; 202 | 203 | // This is where you call the action under test 204 | when('adding 2 to the first number', () => { 205 | const actualResult = addTwo(firstNum); 206 | 207 | // This is where you check the outcome 208 | then('result should be 3', () => { 209 | expect(actualResult).toEqual(3); 210 | }); 211 | }); 212 | }); 213 | 214 | }); 215 | 216 | ``` 217 | 218 | It also prints a nicer test description when there's an error: 219 | 220 | ``` 221 | CONSOLE OUTPUT: 222 | ~~~~~~~~~~~~~~ 223 | 224 | GIVEN first number is 1 225 | WHEN adding 2 to the first number 226 | THEN result should be 3 227 | ``` 228 | 229 |
230 | 231 | ## What's wrong with using `it()` or `test()` for single-action tests? 232 | 233 | Did you know that the most common way of writing JavaScript tests dates back to 2005? 😅 234 | 235 | Jasmine, which was created in 2009 was inspired by Ruby's testing framework - RSpec which was created in 2005. 236 | 237 | Jest, which is based on Jasmine added an alias called `test()` which is basically the same thing. 238 | 239 | Originally, RSpec introduced the syntax of "`describe` > `context` > `it`", where `context` was meant to be used as the "setup" part of the test. 240 | 241 | Unfortunately, the `context` wasn't ported to Jasmine so we got used to writing our tests in the "`describe` > `it`" structure... which is more limited. 242 | 243 |
244 | 245 | _Here are a couple of limitations with the common `it()` structure:_ 246 | 247 | ### ❌ 1. It promotes partial or awkward descriptions of tests 248 | 249 | The word "it" kinda forces you to begin the description with "should" which leads to focusing specifically on just the "outcome" part of the test (the `then`). 250 | 251 | But if you want to add more context (like what should be the input that causes that outcome) things start to get messy. 252 | 253 | Because there isn't a clear convention, people tend to invent their own on the fly which creates inconsistency. 254 | 255 | 256 | **Example:** 257 | ```ts 258 | it('should do X only when environment is Y and also called by Z But only if...you get the point', ()=> ...) 259 | ``` 260 | 261 |
262 | 263 | ### ❌ 2. Nothing prevents you from writing multi-action tests 264 | 265 | This mixes up testing structures and making them harder to understand 266 | 267 | **Example:** 268 | ```ts 269 | it('should transform products', ()=> { 270 | 271 | // SETUP 272 | const fakeProducts = [...]; 273 | 274 | // ACTION 275 | const result = classUnderTest.transformProducts(fakeProducts); 276 | 277 | // OUTCOME 278 | const transformedProducts = [...]; 279 | expect(result).toEqual(transformedProducts); 280 | 281 | // ACTION 282 | const result2 = classUnderTest.transformProducts(); 283 | 284 | // OUTCOME 285 | expect(result2).toEqual( [] ); 286 | 287 | 288 | // this 👆 is a multi-action test. 289 | 290 | }) 291 | ``` 292 |
293 | 294 | ### ❌ 3. Detailed descriptions can get out of date more easily 295 | 296 | The farther the description is from the actual implementation the less likely you'll remember to update it when the test code changes 297 | 298 | **Example:** 299 | ```ts 300 | test('GIVEN valid products and metadata returned successfully WHEN destroying the products THEN they should get decorated', ()=> { 301 | 302 | const fakeProducts = [...]; 303 | const fakeMetadata = [...]; 304 | mySpy.getMetadata.mockReturnValue(fakeMetadata); 305 | 306 | const result = classUnderTest.transformProducts(fakeProducts); 307 | 308 | const decoratedProducts = [...]; 309 | expect(result).toEqual(decoratedProducts); 310 | 311 | }) 312 | ``` 313 |
314 | 315 | #### Did you spot the typo? 👆😅 316 | 317 | 318 | (it should be _"transforming"_ instead of _"destroying"_) 319 | 320 |
321 | 322 | Compare that to - 323 | 324 | ```ts 325 | 326 | given('valid products and metadata returned successfully', () => { 327 | const fakeProducts = [...]; 328 | const fakeMetadata = [...]; 329 | mySpy.getMetadata.mockReturnValue(fakeMetadata); 330 | 331 | // 👇 --> easier to spot as it's closer to the implementation 332 | when('destroying the products', () => { 333 | const result = classUnderTest.transformProducts(fakeProducts); 334 | 335 | then('they should get decorated', () => { 336 | const decoratedProducts = [...]; 337 | expect(result).toEqual(decoratedProducts); 338 | }); 339 | }); 340 | }); 341 | 342 | 343 | ``` 344 | 345 |
346 | 347 | 348 | 349 | ## Usage 350 | 351 | ### ▶ The basic testing structure 352 | 353 | The basic structure is a nesting of these 3 functions: 354 | 355 | ```ts 356 | given(description, () => { 357 | when(description, () => { 358 | then(description, () => { 359 | 360 | }) 361 | }) 362 | }) 363 | 364 | 365 | ``` 366 | 367 | **EXAMPLE:** 368 | ```ts 369 | describe('addTwo', () => { 370 | 371 | // This is where you setup your environment / inputs 372 | given('first number is 1', () => { 373 | const firstNum = 1; 374 | 375 | // This is where you call the action under test 376 | when('adding 2 to the first number', () => { 377 | const actualResult = addTwo(firstNum); 378 | 379 | // This is where you check the outcome 380 | then('result should be 3', () => { 381 | expect(actualResult).toEqual(3); 382 | }); 383 | }); 384 | }); 385 | 386 | }); 387 | 388 | ``` 389 | 390 | Under the hood it creates a regular `it()` test with a combination of all the descriptions: 391 | 392 | ``` 393 | CONSOLE OUTPUT: 394 | ~~~~~~~~~~~~~~ 395 | 396 | GIVEN first number is 1 397 | WHEN adding 2 to the first number 398 | THEN result should be 3 399 | ``` 400 | 401 |
402 | 403 | ### ▶ Meaningful error messages 404 | 405 | This library will throw an error if you deviate from the `given > when > then` structure. 406 | 407 | So you won't be tempted to accidentally turn your single-action test into a multi-action one. 408 | 409 | ```ts 410 | describe('addTwo', () => { 411 | 412 | // 👉 ATTENTION: You cannot start with a "when()" or a "then()" 413 | // the test MUST start with a "given()" 414 | 415 | 416 | given('first number is 1', () => { 417 | const firstNum = 1; 418 | 419 | // 👉 ATTENTION: You cannot add here a "then()" function directly 420 | // or another "given()" function 421 | 422 | when('adding 2 to the first number', () => { 423 | const actualResult = addTwo(firstNum); 424 | 425 | // 👉 ATTENTION: You cannot add here a "given()" function 426 | // or another "when()" function 427 | 428 | then('result should be 3', () => { 429 | expect(actualResult).toEqual(3); 430 | 431 | 432 | // 👉 ATTENTION: You cannot add here a "given()" function 433 | // or a "when()" function or another "then()" 434 | }); 435 | }); 436 | }); 437 | 438 | }); 439 | 440 | ``` 441 | 442 | 443 | ### ▶ `async` / `await` support 444 | 445 | **Example:** 446 | 447 | ```ts 448 | describe('addTwo', () => { 449 | 450 | given('first number is 1', () => { 451 | const firstNum = 1; 452 | // 👇 453 | when('adding 2 to the first number', async () => { 454 | const actualResult = await addTwo(firstNum); 455 | 456 | then('result should be 3', () => { 457 | expect(actualResult).toEqual(3); 458 | }); 459 | }); 460 | }); 461 | 462 | }); 463 | 464 | 465 | ``` 466 | 467 | ### ▶ `done()` function support 468 | 469 | The `given` function supports the (old) async callback way of using a `done()` function to signal when the test is completed. 470 | 471 | ```ts 472 | describe('addTwo', () => { 473 | // 👇 474 | given('first number is 1', (done) => { 475 | const firstNum = 1; 476 | 477 | when('adding 2 to the first number', () => { 478 | const actualResult = addTwo(firstNum, function callback() { 479 | 480 | then('result should be 3', () => { 481 | expect(actualResult).toEqual(3); 482 | done(); 483 | }); 484 | 485 | }); 486 | }); 487 | 488 | }); 489 | 490 | }); 491 | 492 | 493 | ``` 494 | 495 | ℹ It also supports `done(error)` or `done.fail(error)` for throwing async errors. 496 | 497 |
498 | 499 | ## Contributing 500 | 501 | Want to contribute? Yayy! 🎉 502 | 503 | Please read and follow our [Contributing Guidelines](../../CONTRIBUTING.md) to learn what are the right steps to take before contributing your time, effort and code. 504 | 505 | Thanks 🙏 506 | 507 |
508 | 509 | ## Code Of Conduct 510 | 511 | Be kind to each other and please read our [code of conduct](../../CODE_OF_CONDUCT.md). 512 | 513 | 514 |
515 | 516 | ## Contributors ✨ 517 | 518 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 |

Shai Reznik

💻 📖 🤔 🚇 🚧 🧑‍🏫 👀 ⚠️

Maarten Tibau

📖 🚇

Benjamin Gruenbaum

💻 🤔 🚧
530 | 531 | 532 | 533 | 534 | 535 | 536 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 537 | 538 |
539 | 540 | ## License 541 | 542 | MIT 543 | -------------------------------------------------------------------------------- /packages/jest-single/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | rootDir: '../../', 4 | roots: ['/shared/single-core', '/packages/jest-single/spec'], 5 | verbose: true, 6 | resetMocks: true, 7 | testEnvironment: 'node', 8 | transform: { 9 | '^.+\\.[tj]s?$': ['ts-jest', { 10 | tsconfig: '/packages/jest-single/tsconfig.json' 11 | }] 12 | }, 13 | collectCoverage: true, 14 | coverageDirectory: 'packages/jest-single/coverage', 15 | coverageThreshold: { 16 | global: { 17 | branches: 100, 18 | functions: 90, 19 | lines: 90, 20 | statements: 90, 21 | }, 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/jest-single/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hirez_io/jest-single", 3 | "version": "1.1.8", 4 | "publishConfig": { 5 | "access": "public", 6 | "registry": "https://registry.npmjs.org" 7 | }, 8 | "author": { 9 | "name": "Shai Reznik", 10 | "company": "HiRez.io" 11 | }, 12 | "description": "A jest addon that helps you write 'Single-Action Tests' by breaking them into a given / when / then structure.", 13 | "keywords": [ 14 | "jest", 15 | "jest-single", 16 | "gwt", 17 | "Given When Then", 18 | "Microtests", 19 | "Testing", 20 | "Unit tests", 21 | "JavasScript Unit Tests", 22 | "TypeScript Unit Tests", 23 | "hirez.io" 24 | ], 25 | "homepage": "https://github.com/hirezio/single/tree/main/packages/jest-single", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/hirezio/single", 29 | "directory": "packages/jest-single" 30 | }, 31 | "license": "MIT", 32 | "main": "dist/jest-single.js", 33 | "types": "dist/jest-single.d.ts", 34 | "scripts": { 35 | "clean": "rimraf dist", 36 | "clean:temp-src": "rimraf temp-src", 37 | "copy:shared-source": "cpy ../../shared/single-core/single-core.ts ./temp-src/ --rename=jest-single.ts", 38 | "compile": "tsc -p tsconfig.build.json", 39 | "build": "run-s clean copy:shared-source compile clean:temp-src", 40 | "test": "echo \n*** Run tests from the root folder\n" 41 | }, 42 | "peerDependencies": { 43 | "jest": "< 30.x" 44 | }, 45 | "devDependencies": { 46 | "@types/jest": "29.5.3", 47 | "cpy-cli": "^3.1.1", 48 | "jest": "29.6.3", 49 | "microbundle": "^0.15.0", 50 | "ts-jest": "29.1.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/jest-single/spec/jest-single.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getOnlyOneError, 3 | getMustBeAChildError 4 | } from '../../../shared/single-core/single-core'; 5 | 6 | const root = (1, eval)('this'); 7 | 8 | describe('Jest Single', () => { 9 | function addTwo(num: number | undefined) { 10 | if (num) { 11 | return num + 2; 12 | } 13 | return undefined; 14 | } 15 | 16 | function addTwoAsync(num: number | undefined): Promise { 17 | if (num) { 18 | return Promise.resolve(num + 2); 19 | } 20 | return Promise.resolve(); 21 | } 22 | 23 | describe('A synchronous test', () => { 24 | given('input is set to 1', () => { 25 | const fakeNumber = 1; 26 | 27 | when(`adding 2`, () => { 28 | const actualResult = addTwo(fakeNumber); 29 | 30 | then('result should be 3', () => { 31 | expect(actualResult).toBe(3); 32 | }); 33 | }); 34 | }); 35 | 36 | given('input is set to 1', () => { 37 | const fakeNumber = 1; 38 | 39 | when('adding 2', () => { 40 | const actualResult = addTwo(fakeNumber); 41 | 42 | then('result should be 3', () => { 43 | expect(actualResult).toBe(3); 44 | }); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('full description', () => { 50 | it('should combine all 3 descriptions', () => { 51 | const itSpy = jest.spyOn(root, 'it').mockImplementation(); 52 | given('fake given description', () => { 53 | when(`fake when description`, () => { 54 | then('fake then description', () => {}); 55 | }); 56 | }); 57 | const expectedFullDescription = ` 58 | GIVEN fake given description 59 | WHEN fake when description 60 | THEN fake then description`; 61 | 62 | const actualDescription = itSpy.mock.calls[0][0]; 63 | expect(actualDescription).toEqual(expectedFullDescription); 64 | }); 65 | }); 66 | 67 | describe('focus and exclude', () => { 68 | it('should be able to focus on 1 given using "fgiven"', () => { 69 | const fitSpy = jest.spyOn(root, 'fit').mockImplementation(); 70 | fgiven('FAKE DESCRIPTION', function () {}); 71 | 72 | expect(fitSpy).toHaveBeenCalled(); 73 | }); 74 | 75 | it('should be able to focus on 1 given using "given.only"', () => { 76 | const fitSpy = jest.spyOn(root, 'fit').mockImplementation();; 77 | given.only('FAKE DESCRIPTION', function () {}); 78 | 79 | expect(fitSpy).toHaveBeenCalled(); 80 | }); 81 | 82 | it('should not allow "fgiven" not as a root given', () => { 83 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => fn()); 84 | 85 | given('root given', () => { 86 | try { 87 | fgiven('another given', () => {}); 88 | } catch (error: any) { 89 | expect(error.message).toEqual(getOnlyOneError('given')); 90 | } 91 | }); 92 | }); 93 | 94 | it('should combine all 3 descriptions for fgiven as well', () => { 95 | const itSpy = jest.spyOn(root, 'fit').mockImplementation(); 96 | fgiven('fake given description', () => { 97 | when(`fake when description`, () => { 98 | then('fake then description', () => {}); 99 | }); 100 | }); 101 | const expectedFullDescription = ` 102 | GIVEN fake given description 103 | WHEN fake when description 104 | THEN fake then description`; 105 | 106 | const actualDescription = itSpy.mock.calls[0][0]; 107 | expect(actualDescription).toEqual(expectedFullDescription); 108 | }); 109 | 110 | it('should be able to exclude a given using "xgiven"', () => { 111 | const xitSpy = jest.spyOn(root, 'xit').mockImplementation(); 112 | xgiven('FAKE DESCRIPTION', function () {}); 113 | 114 | expect(xitSpy).toHaveBeenCalled(); 115 | }); 116 | 117 | it('should be able to exclude a given using "given.skip"', () => { 118 | const xitSpy = jest.spyOn(root, 'xit').mockImplementation(); 119 | given.skip('FAKE DESCRIPTION', function () {}); 120 | 121 | expect(xitSpy).toHaveBeenCalled(); 122 | }); 123 | }); 124 | 125 | describe('Async tests', () => { 126 | 127 | given('a "done" function gets passed', (done) => { 128 | const fakeNumber = 1; 129 | 130 | when('adding 2 asynchronously', () => { 131 | const actualResult = addTwo(fakeNumber); 132 | 133 | setTimeout(() => { 134 | then('result should be 3', () => { 135 | expect(actualResult).toBe(3); 136 | done(); 137 | }); 138 | }, 0); 139 | 140 | }); 141 | }); 142 | 143 | it('should handle errors passed to "done"', async () => { 144 | let actualPromiseFromGiven: Promise | undefined = undefined; 145 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => { 146 | actualPromiseFromGiven = fn(); 147 | }); 148 | 149 | given('given', (done) => { 150 | when('when', () => { 151 | then('then', () => { 152 | done(new Error('FAKE ERROR')); 153 | }); 154 | }); 155 | }); 156 | try { 157 | await actualPromiseFromGiven; 158 | 159 | } catch (error: any) { 160 | expect(error.message).toEqual('FAKE ERROR'); 161 | } 162 | }); 163 | 164 | it('should handle errors passed to "done.fail()"', async () => { 165 | let actualPromiseFromGiven: Promise | undefined = undefined; 166 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => { 167 | actualPromiseFromGiven = fn(); 168 | }); 169 | 170 | given('given', (done) => { 171 | when('when', () => { 172 | then('then', () => { 173 | done.fail(new Error('FAKE ERROR')); 174 | }); 175 | }); 176 | }); 177 | try { 178 | await actualPromiseFromGiven; 179 | 180 | } catch (error: any) { 181 | expect(error.message).toEqual('FAKE ERROR'); 182 | } 183 | }); 184 | 185 | it('should handle promise errors thrown inside "when"', async () => { 186 | let actualPromiseFromGiven: Promise | undefined = undefined; 187 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => { 188 | actualPromiseFromGiven = fn(); 189 | }); 190 | 191 | given('given', async () => { 192 | when('when', async () => { 193 | await Promise.resolve().then(() => { 194 | throw new Error('FAKE ERROR'); 195 | }) 196 | 197 | }) 198 | }); 199 | try { 200 | await actualPromiseFromGiven; 201 | 202 | } catch (error: any) { 203 | expect(error.message).toEqual('FAKE ERROR'); 204 | } 205 | }); 206 | 207 | it('should handle promise errors thrown inside "then"', async () => { 208 | let actualPromiseFromGiven: Promise | undefined = undefined; 209 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => { 210 | actualPromiseFromGiven = fn(); 211 | }); 212 | 213 | given('given', async () => { 214 | when('when', async () => { 215 | then('then', async () => { 216 | await Promise.resolve().then(() => { 217 | throw new Error('FAKE ERROR'); 218 | }) 219 | 220 | }) 221 | }) 222 | }); 223 | try { 224 | await actualPromiseFromGiven; 225 | 226 | } catch (error: any) { 227 | expect(error.message).toEqual('FAKE ERROR'); 228 | } 229 | }); 230 | 231 | given('input is set to 1', () => { 232 | const fakeNumber = 1; 233 | 234 | when('adding 2 asynchronously', async () => { 235 | const actualResult = await addTwoAsync(fakeNumber); 236 | const actualResult2 = await addTwoAsync(fakeNumber); 237 | 238 | then('result should be 3', () => { 239 | expect(actualResult).toBe(3); 240 | expect(actualResult2).toBe(3); 241 | }); 242 | }); 243 | }); 244 | 245 | given('input is set to async 1', async () => { 246 | const fakeNumber = await 1; 247 | 248 | when('adding 2 asynchronously', async () => { 249 | const actualResult = await addTwoAsync(fakeNumber); 250 | 251 | then('async result should be 3', async () => { 252 | const expectedResult = await Promise.resolve(3); 253 | expect(actualResult).toBe(expectedResult); 254 | }); 255 | }); 256 | }); 257 | 258 | }); 259 | 260 | describe('wrong order of functions', () => { 261 | it('should not allow "given" inside of another "given"', () => { 262 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => fn()); 263 | 264 | given('root given', () => { 265 | try { 266 | given('another given', () => {}); 267 | } catch (error: any) { 268 | expect(error.message).toEqual(getOnlyOneError('given')); 269 | } 270 | }); 271 | }); 272 | 273 | it('should not allow "given" inside of another "when"', () => { 274 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => fn()); 275 | 276 | given('root given', () => { 277 | when('fake when', () => { 278 | try { 279 | given('another given', () => {}); 280 | } catch (error: any) { 281 | expect(error.message).toEqual(getOnlyOneError('given')); 282 | } 283 | }); 284 | }); 285 | }); 286 | 287 | it('should not allow "given" inside of another "then"', () => { 288 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => fn()); 289 | 290 | given('root given', () => { 291 | when('fake when', () => { 292 | then('fake then', () => { 293 | try { 294 | given('another given', () => {}); 295 | } catch (error: any) { 296 | expect(error.message).toEqual(getOnlyOneError('given')); 297 | } 298 | }); 299 | }); 300 | }); 301 | }); 302 | 303 | it('should not allow "when" without a "given" parent', () => { 304 | try { 305 | when('fake when', () => {}); 306 | fail('The function "when" should have thrown an error'); 307 | } catch (error: any) { 308 | expect(error.message).toEqual(getMustBeAChildError('when')); 309 | } 310 | }); 311 | 312 | it('should not allow "then" without a "when" parent', () => { 313 | try { 314 | then('fake then', () => {}); 315 | fail('The function "then" should have thrown an error'); 316 | } catch (error: any) { 317 | expect(error.message).toEqual(getMustBeAChildError('then')); 318 | } 319 | }); 320 | it('should not allow "then" straight under a "given"', () => { 321 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => fn()); 322 | given('root given', () => { 323 | try { 324 | then('fake then', () => {}); 325 | fail('The function "then" should have thrown an error'); 326 | } catch (error: any) { 327 | expect(error.message).toEqual(getMustBeAChildError('then')); 328 | } 329 | }); 330 | }); 331 | 332 | it('should not allow 2 "when" functions under 1 "given"', () => { 333 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => fn()); 334 | given('root given', () => { 335 | try { 336 | when('first when', () => {}); 337 | when('second when', () => {}); 338 | fail('The second "when" function should have thrown an error'); 339 | } catch (error: any) { 340 | expect(error.message).toEqual(getOnlyOneError('when')); 341 | } 342 | }); 343 | }); 344 | 345 | it('should not allow 2 "then" functions under 1 "when"', () => { 346 | jest.spyOn(root, 'it').mockImplementation((desc: any, fn: any) => fn()); 347 | given('root given', () => { 348 | when('when', () => { 349 | try { 350 | then('first then', () => {}); 351 | then('second then', () => {}); 352 | fail('The second "then" function should have thrown an error'); 353 | } catch (error: any) { 354 | expect(error.message).toEqual(getOnlyOneError('then')); 355 | } 356 | }); 357 | }); 358 | }); 359 | 360 | }); 361 | 362 | 363 | }); 364 | -------------------------------------------------------------------------------- /packages/jest-single/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./temp-src" 5 | }, 6 | "include": [ 7 | "temp-src" 8 | ], 9 | "exclude": ["**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/jest-single/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "declaration": true, // generate .d.ts files 9 | "declarationMap": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": false, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "sourceMap": true, 15 | "downlevelIteration": true, 16 | "lib": ["es2015", "es2016", "dom"], 17 | "pretty": true, 18 | "strict": true, 19 | "baseUrl": ".", 20 | "outDir": "dist", 21 | "types": ["jest", "node"], 22 | "typeRoots": [ 23 | "../../node_modules/@hirez_io/jest-single", 24 | "../../node_modules/@types" 25 | ] 26 | }, 27 | "include": ["spec"], 28 | } 29 | -------------------------------------------------------------------------------- /packages/karma-jasmine-single/.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "single", 3 | "projectOwner": "hirezio", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md", 8 | "../../README.md" 9 | ], 10 | "imageSize": 100, 11 | "commit": true, 12 | "commitConvention": "angular", 13 | "contributors": [ 14 | { 15 | "login": "shairez", 16 | "name": "Shai Reznik", 17 | "avatar_url": "https://avatars1.githubusercontent.com/u/1430726?v=4", 18 | "profile": "https://www.hirez.io/?utm_medium=Open_Source&utm_source=Github&utm_campaign=Lead_Generation&utm_content=karma-jasmine-single--all-contributors-profile-link", 19 | "contributions": [ 20 | "code", 21 | "doc", 22 | "ideas", 23 | "infra", 24 | "maintenance", 25 | "mentoring", 26 | "review", 27 | "test" 28 | ] 29 | } 30 | ], 31 | "contributorsPerLine": 7 32 | } 33 | -------------------------------------------------------------------------------- /packages/karma-jasmine-single/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output -------------------------------------------------------------------------------- /packages/karma-jasmine-single/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.0.4](https://github.com/hirezio/single/compare/@hirez_io/karma-jasmine-single@1.0.3...@hirez_io/karma-jasmine-single@1.0.4) (2023-08-25) 7 | 8 | **Note:** Version bump only for package @hirez_io/karma-jasmine-single 9 | 10 | 11 | 12 | 13 | 14 | ## [1.0.3](https://github.com/hirezio/single/compare/@hirez_io/karma-jasmine-single@1.0.2...@hirez_io/karma-jasmine-single@1.0.3) (2022-03-13) 15 | 16 | **Note:** Version bump only for package @hirez_io/karma-jasmine-single 17 | 18 | 19 | 20 | 21 | 22 | ## [1.0.2](https://github.com/hirezio/single/compare/@hirez_io/karma-jasmine-single@1.0.1...@hirez_io/karma-jasmine-single@1.0.2) (2022-03-13) 23 | 24 | **Note:** Version bump only for package @hirez_io/karma-jasmine-single 25 | 26 | 27 | 28 | 29 | 30 | ## [1.0.1](https://github.com/hirezio/single/compare/@hirez_io/karma-jasmine-single@1.0.0...@hirez_io/karma-jasmine-single@1.0.1) (2021-08-08) 31 | 32 | **Note:** Version bump only for package @hirez_io/karma-jasmine-single 33 | 34 | 35 | 36 | 37 | 38 | # 1.0.0 (2021-07-29) 39 | 40 | **Note:** Version bump only for package @hirez_io/karma-jasmine-single 41 | -------------------------------------------------------------------------------- /packages/karma-jasmine-single/README.md: -------------------------------------------------------------------------------- 1 | # `@hirez_io/karma jasmine-single` ⚒ 2 | 3 | A karma plugin for loading [@hirez_io/jasmine-single](https://github.com/hirezio/single/tree/main/packages/jasmine-single) 4 | 5 | [![npm version](https://img.shields.io/npm/v/@hirez_io/karma-jasmine-single.svg?style=flat-square)](https://www.npmjs.org/package/@hirez_io/karma-jasmine-single) 6 | [![npm downloads](https://img.shields.io/npm/dm/@hirez_io/karma-jasmine-single.svg?style=flat-square)](http://npm-stat.com/charts.html?package=@hirez_io/karma-jasmine-single&from=2017-07-26) 7 | ![Build and optionally publish](https://github.com/hirezio/single/workflows/Build%20and%20optionally%20publish/badge.svg) 8 | [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) 9 | [![Code of Conduct](https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square)](../../CODE_OF_CONDUCT.md) 10 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 11 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-green.svg?style=flat-square)](#contributors-) 12 | 13 | 14 |
15 | 16 | TestAngular.com - Free Angular Testing Workshop - The Roadmap to Angular Testing Mastery 20 | 21 |
22 | 23 | ## Installation 24 | 25 | ``` 26 | yarn add -D @hirez_io/karma-jasmine-single 27 | ``` 28 | 29 | or 30 | 31 | ``` 32 | npm install -D @hirez_io/karma-jasmine-single 33 | ``` 34 | 35 | This plugin was inspired by [karma-jasmine-single](https://github.com/kirisu/karma-jasmine-single)) which loads the original "jasmine-single". 36 | 37 | I rewrote it to save you the hassle of loading @hirez_io/jasmine-single's script files yourself. 😎 38 | 39 | ## Configuration 40 | 41 | Here's how to modify your `karma.conf.js`: 42 | 43 | ```js 44 | // karma.conf.js 45 | 46 | module.exports = function(config) { 47 | config.set({ 48 | 49 | plugins: [ 50 | require('karma-jasmine'), 51 | require('@hirez_io/karma-jasmine-single'), // <-- ADD THIS 52 | require('karma-chrome-launcher') 53 | // other plugins you might have... 54 | ], 55 | 56 | frameworks: [ 57 | '@hirez_io/jasmine-single', // <-- ADD THIS 58 | 'jasmine', 59 | // other frameworks... 60 | ], 61 | 62 | // ... 63 | ``` 64 | 65 | Want to contribute? Yayy! 🎉 66 | 67 | Please read and follow our [Contributing Guidelines](../../CONTRIBUTING.md) to learn what are the right steps to take before contributing your time, effort and code. 68 | 69 | Thanks 🙏 70 | 71 |
72 | 73 | ## Code Of Conduct 74 | 75 | Be kind to each other and please read our [code of conduct](../../CODE_OF_CONDUCT.md). 76 | 77 | 78 |
79 | 80 | ## Contributors ✨ 81 | 82 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |

Shai Reznik

💻 📖 🤔 🚇 🚧 🧑‍🏫 👀 ⚠️
92 | 93 | 94 | 95 | 96 | 97 | 98 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 99 | 100 |
101 | 102 | ## License 103 | 104 | MIT 105 | -------------------------------------------------------------------------------- /packages/karma-jasmine-single/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Aug 11 2020 14:21:54 GMT+0300 (Israel Daylight Time) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | plugins: [ 8 | require('karma-jasmine'), 9 | require('@hirez_io/karma-jasmine-single'), 10 | require('karma-chrome-launcher') 11 | ], 12 | 13 | // base path that will be used to resolve all patterns (eg. files, exclude) 14 | basePath: '', 15 | 16 | 17 | // frameworks to use 18 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 19 | frameworks: ['@hirez_io/jasmine-single', 'jasmine'], 20 | 21 | // list of files / patterns to load in the browser 22 | files: [ 23 | './lib/karma-jasmine-single.spec.js' 24 | ], 25 | 26 | // test results reporter to use 27 | // possible values: 'dots', 'progress' 28 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 29 | reporters: ['progress'], 30 | 31 | 32 | // web server port 33 | port: 9876, 34 | 35 | 36 | // enable / disable colors in the output (reporters and logs) 37 | colors: true, 38 | 39 | 40 | // level of logging 41 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 42 | logLevel: config.LOG_INFO, 43 | 44 | 45 | // enable / disable watching file and executing tests whenever any file changes 46 | autoWatch: false, 47 | 48 | 49 | // start these browsers 50 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 51 | browsers: ['ChromeHeadless'], 52 | // browsers: ['Chrome'], 53 | 54 | 55 | // Continuous Integration mode 56 | // if true, Karma captures browsers, runs the tests and exits 57 | singleRun: true, 58 | 59 | // Concurrency level 60 | // how many browser should be started simultaneous 61 | concurrency: Infinity 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /packages/karma-jasmine-single/lib/karma-jasmine-single.js: -------------------------------------------------------------------------------- 1 | var createPattern = function (file) { 2 | return { 3 | pattern: file, 4 | included: true, 5 | served: true, 6 | watched: false, 7 | }; 8 | }; 9 | 10 | var initJasmineSingle = function (files) { 11 | const jasmineSingleFile = createPattern(require.resolve('@hirez_io/jasmine-single')); 12 | 13 | // Find "karma-jasmine" last file (adapter.js) to make sure 14 | // "jasmine" is loaded before "jasmine-single" 15 | const karmaJasmineAdapterFileIndex = files.findIndex((file) => { 16 | return file.pattern.indexOf('adapter.js') !== -1; 17 | }); 18 | 19 | if (karmaJasmineAdapterFileIndex !== -1) { 20 | files.splice(karmaJasmineAdapterFileIndex + 1, 0, jasmineSingleFile); 21 | } else { 22 | files.unshift(jasmineSingleFile); 23 | } 24 | }; 25 | 26 | initJasmineSingle.$inject = ['config.files']; 27 | 28 | module.exports = { 29 | 'framework:@hirez_io/jasmine-single': ['factory', initJasmineSingle], 30 | 'framework:@hirez_io/karma-jasmine-single': ['factory', initJasmineSingle], 31 | }; 32 | -------------------------------------------------------------------------------- /packages/karma-jasmine-single/lib/karma-jasmine-single.spec.js: -------------------------------------------------------------------------------- 1 | describe('Karma Jasmine Single', function(){ 2 | 3 | let num; 4 | 5 | Given(function () { 6 | num = 1; 7 | }); 8 | 9 | When(function(){ 10 | num++; 11 | }); 12 | 13 | Then(function(){ 14 | expect(num).toBe(2); 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /packages/karma-jasmine-single/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hirez_io/karma-jasmine-single", 3 | "version": "1.0.4", 4 | "publishConfig": { 5 | "access": "public", 6 | "registry": "https://registry.npmjs.org" 7 | }, 8 | "author": { 9 | "name": "Shai Reznik", 10 | "company": "HiRez.io" 11 | }, 12 | "description": "A karma plugin for loading @hirez_io/jasmine-single", 13 | "keywords": [ 14 | "jasmine", 15 | "jasmine-single", 16 | "jasmine-karma", 17 | "karma", 18 | "gwt", 19 | "Given When Then", 20 | "Microtests", 21 | "Testing", 22 | "Unit tests", 23 | "JavasScript Unit Tests", 24 | "TypeScript Unit Tests", 25 | "hirez.io" 26 | ], 27 | "homepage": "https://github.com/hirezio/single/tree/main/packages/karma-jasmine-single", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/hirezio/single", 31 | "directory": "packages/karma-jasmine-single" 32 | }, 33 | "license": "MIT", 34 | "main": "lib/karma-jasmine-single.js", 35 | "scripts": { 36 | "build": "echo build is not needed for the karma plugin", 37 | "test": "karma start" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/shairez/single/issues" 41 | }, 42 | "devDependencies": { 43 | "karma": "6.3.17", 44 | "karma-chrome-launcher": "3.1.1", 45 | "karma-jasmine": "4.0.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /shared/single-core/single-core.ts: -------------------------------------------------------------------------------- 1 | export function getOnlyOneError(funcName: string): string { 2 | return `There must be only 1 "${funcName}" per test`; 3 | } 4 | 5 | export function getMustBeAChildError(funcName: string): string { 6 | let parentName = 'given'; 7 | if (funcName === 'then') { 8 | parentName = 'when'; 9 | } 10 | return `"${funcName}" must be a child of the function "${parentName}"`; 11 | } 12 | 13 | export const WHEN_IS_ASYNC_ERROR = ` 14 | ==================================== 15 | You wrapped the "when" callback with "async". 16 | Please wrap the "given" callback with async as well and run "when" with await like this: 17 | 18 | 👇 19 | given('description', async () => { 20 | 👇 21 | await when('description', async () => { 22 | 23 | ... your "await" code ... 24 | 25 | }); 26 | }) 27 | ==================================== 28 | ` 29 | 30 | declare global { 31 | const given: GivenFunction; 32 | const fgiven: TestFunction; 33 | const xgiven: TestFunction; 34 | const when: TestFunction; 35 | const then: TestFunction; 36 | } 37 | 38 | export interface TestFunction{ 39 | (description: string, callback: TestCallback): void 40 | } 41 | 42 | export interface GivenFunction extends TestFunction { 43 | only: TestFunction; 44 | skip: TestFunction; 45 | } 46 | 47 | export interface DoneFn { 48 | (...args: Error[]): void; 49 | fail: (...args: Error[]) => void; 50 | } 51 | 52 | export interface TestCallback { 53 | (done: DoneFn, ...args:any[]): any; 54 | } 55 | 56 | const root = (1, eval)('this'); 57 | 58 | let currentRunningFunction: 'given' | 'when' | 'then' | undefined; 59 | let numberOfWhens: number; 60 | let numberOfThens: number; 61 | let doneFunctionIsSet: boolean = false; 62 | let innerPromiseQueue: Promise[]; 63 | 64 | beforeEach(function () { 65 | currentRunningFunction = undefined; 66 | numberOfWhens = 0; 67 | numberOfThens = 0; 68 | doneFunctionIsSet = false; 69 | innerPromiseQueue = []; 70 | }); 71 | 72 | root.given = function given(givenDescription: string, callback: (...args:any[])=>any) { 73 | if (currentRunningFunction !== undefined) { 74 | throw new Error(getOnlyOneError('given')); 75 | } 76 | 77 | const fullDescription = getFullDescription(givenDescription, callback); 78 | it(fullDescription, getGivenCallbackWrapper(callback)); 79 | }; 80 | 81 | root.given.only = root.fgiven = function given(givenDescription: string, callback: (...args:any[])=>any) { 82 | if (currentRunningFunction !== undefined) { 83 | throw new Error(getOnlyOneError('given')); 84 | } 85 | const fullDescription = getFullDescription(givenDescription, callback); 86 | fit(fullDescription, getGivenCallbackWrapper(callback)); 87 | }; 88 | 89 | function getFullDescription(givenDescription: string, callback: (...args:any[])=>any): string{ 90 | const functionContent = callback.toString(); 91 | const regex = /when\(["'`](.*?)["'`],.*then\(["'`](.*?)["'`],/mgs; 92 | 93 | const match = regex.exec(functionContent); 94 | 95 | let fullDescription = ` 96 | GIVEN ${givenDescription}`; 97 | 98 | if (match) { 99 | /* istanbul ignore next */ 100 | if (match[1]) { 101 | fullDescription += ` 102 | WHEN ${match[1]}`; 103 | } 104 | /* istanbul ignore next */ 105 | if (match[2]) { 106 | fullDescription += ` 107 | THEN ${match[2]}`; 108 | } 109 | } 110 | 111 | return fullDescription; 112 | } 113 | 114 | function getGivenCallbackWrapper(callback: (...args:any[])=>any) { 115 | return async function givenCallbackWrapper() { 116 | 117 | currentRunningFunction = 'given'; 118 | 119 | await promisify(callback); 120 | 121 | if (innerPromiseQueue.length > 0) { 122 | for (const promise of innerPromiseQueue) { 123 | 124 | await promise; 125 | 126 | } 127 | } 128 | 129 | } 130 | } 131 | 132 | root.given.skip = root.xgiven = function given(description: string, callback: any) { 133 | xit(description, callback); 134 | }; 135 | 136 | 137 | root.when = function when(description: string, callback: (...args:any[])=>any) { 138 | numberOfWhens++; 139 | if (numberOfWhens > 1) { 140 | throw new Error(getOnlyOneError('when')); 141 | } 142 | 143 | if (currentRunningFunction !== 'given') { 144 | throw new Error(getMustBeAChildError('when')); 145 | } 146 | 147 | const oldContext = currentRunningFunction; 148 | currentRunningFunction = 'when'; 149 | 150 | const potentialPromise = callback(); 151 | if (potentialPromise) { 152 | const then = potentialPromise.then; 153 | /* istanbul ignore else */ 154 | if (typeof then === 'function') { 155 | const whenPromise = then.call(potentialPromise, () => { 156 | currentRunningFunction = oldContext; 157 | }) 158 | 159 | innerPromiseQueue.push(whenPromise); 160 | 161 | } else { 162 | /* istanbul ignore next */ 163 | currentRunningFunction = oldContext; 164 | } 165 | } else { 166 | currentRunningFunction = oldContext; 167 | } 168 | }; 169 | 170 | root.then = function then(description: string, callback: (...args:any[])=>any) { 171 | numberOfThens++; 172 | if (numberOfThens > 1) { 173 | throw new Error(getOnlyOneError('then')); 174 | } 175 | 176 | if (!doneFunctionIsSet && currentRunningFunction !== 'when') { 177 | throw new Error(getMustBeAChildError('then')); 178 | } 179 | 180 | const oldContext = currentRunningFunction; 181 | currentRunningFunction = 'then'; 182 | 183 | const potentialPromise = callback(); 184 | if (potentialPromise) { 185 | const then = potentialPromise.then; 186 | /* istanbul ignore else */ 187 | if (typeof then === 'function') { 188 | const thenPromise = then.call(potentialPromise, () => { 189 | currentRunningFunction = oldContext; 190 | }); 191 | innerPromiseQueue.push(thenPromise); 192 | } else { 193 | /* istanbul ignore next */ 194 | currentRunningFunction = oldContext; 195 | } 196 | } else { 197 | currentRunningFunction = oldContext; 198 | } 199 | }; 200 | 201 | async function promisify(fn: TestCallback): Promise { 202 | if (doesFunctionHaveParams(fn)) { 203 | doneFunctionIsSet = true; 204 | return new Promise((resolve, reject) => { 205 | function next(err: Error) { 206 | if (err) { 207 | reject(err); 208 | return; 209 | } 210 | resolve(undefined as any); 211 | } 212 | next.fail = function nextFail(err: Error) { 213 | reject(err); 214 | }; 215 | 216 | fn.call(this, next); 217 | }); 218 | } 219 | return await (fn as () => any).call(this); 220 | } 221 | 222 | function doesFunctionHaveParams(fn: (...args: any[]) => any) { 223 | return fn.length > 0; 224 | } 225 | -------------------------------------------------------------------------------- /update-core-hash.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const fs = require('fs').promises; 4 | const execa = require('execa'); 5 | 6 | (async () => { 7 | try { 8 | const coreFileName = 'shared/single-core/single-core.ts'; 9 | const jestCacheOfHashFileName = 'packages/jest-single/.core-version'; 10 | const jasmineCacheOfHashFileName = 'packages/jasmine-single/.core-version'; 11 | 12 | const {stdout} = await execa('git', ['hash-object', coreFileName]); 13 | const latestCoreHash = stdout; 14 | 15 | const jestCachedHash = await fs.readFile(jestCacheOfHashFileName, 'utf8'); 16 | const jasmineCachedHash = await fs.readFile(jasmineCacheOfHashFileName, 'utf8'); 17 | 18 | if (jestCachedHash === latestCoreHash && jasmineCachedHash === latestCoreHash) { 19 | console.log('No changes in single-core'); 20 | return; 21 | } 22 | 23 | await fs.writeFile(jestCacheOfHashFileName, latestCoreHash); 24 | await fs.writeFile(jasmineCacheOfHashFileName, latestCoreHash); 25 | 26 | console.log( 27 | 'Updated single-core hash to help lerna know that it needs to bump versions' 28 | ); 29 | 30 | // await execa('git', ['add', jestCacheOfHashFileName, jasmineCacheOfHashFileName]); 31 | 32 | // const { stdout } = await execa('git', ['commit', '--amend', '--no-edit']); 33 | console.log(stdout); 34 | } catch (error) { 35 | console.error(error); 36 | } 37 | })(); 38 | --------------------------------------------------------------------------------