├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README v5 preview.md ├── README.md ├── bin └── ospec ├── changelog.md ├── ospec.js ├── package-lock.json ├── package.json ├── releasing.md ├── scripts ├── build-done-parser.js ├── logger.js └── rename-stable-binaries.js └── tests ├── fixtures ├── legacy │ ├── README.md │ ├── metadata │ │ ├── cjs │ │ │ ├── default1.js │ │ │ ├── default2.js │ │ │ ├── override.js │ │ │ └── package.json │ │ ├── config.js │ │ └── esm │ │ │ ├── default1.js │ │ │ ├── default2.js │ │ │ ├── override.js │ │ │ └── package.json │ ├── success │ │ ├── cjs │ │ │ ├── explicit │ │ │ │ ├── explicit1.js │ │ │ │ └── explicit2.js │ │ │ ├── main.js │ │ │ ├── other.js │ │ │ ├── package.json │ │ │ ├── tests │ │ │ │ ├── main1.js │ │ │ │ └── main2.js │ │ │ └── very │ │ │ │ └── deep │ │ │ │ └── tests │ │ │ │ ├── deep1.js │ │ │ │ └── deeper │ │ │ │ └── deep2.js │ │ ├── config.js │ │ └── esm │ │ │ ├── explicit │ │ │ ├── explicit1.js │ │ │ └── explicit2.js │ │ │ ├── main.js │ │ │ ├── other.js │ │ │ ├── package.json │ │ │ ├── tests │ │ │ ├── main1.js │ │ │ └── main2.js │ │ │ └── very │ │ │ └── deep │ │ │ └── tests │ │ │ ├── deep1.js │ │ │ └── deeper │ │ │ └── deep2.js │ └── throws │ │ ├── cjs │ │ ├── main.js │ │ ├── other.js │ │ ├── package.json │ │ ├── tests │ │ │ ├── main1.js │ │ │ └── main2.js │ │ └── very │ │ │ └── deep │ │ │ └── tests │ │ │ ├── deep1.js │ │ │ └── deeper │ │ │ └── deep2.js │ │ ├── config.js │ │ └── esm │ │ ├── main.js │ │ ├── other.js │ │ ├── package.json │ │ ├── tests │ │ ├── main1.js │ │ └── main2.js │ │ └── very │ │ └── deep │ │ └── tests │ │ ├── deep1.js │ │ └── deeper │ │ └── deep2.js └── v5 │ ├── README.md │ ├── metadata │ ├── cjs │ │ ├── default1.js │ │ ├── default2.js │ │ ├── override.js │ │ └── package.json │ ├── config.js │ └── esm │ │ ├── default1.js │ │ ├── default2.js │ │ ├── override.js │ │ └── package.json │ ├── success │ ├── cjs │ │ ├── explicit │ │ │ ├── explicit1.js │ │ │ └── explicit2.js │ │ ├── main.js │ │ ├── other.js │ │ ├── package.json │ │ ├── tests │ │ │ ├── main1.js │ │ │ └── main2.js │ │ └── very │ │ │ └── deep │ │ │ └── tests │ │ │ ├── deep1.js │ │ │ └── deeper │ │ │ └── deep2.js │ ├── config.js │ └── esm │ │ ├── explicit │ │ ├── explicit1.js │ │ └── explicit2.js │ │ ├── main.js │ │ ├── other.js │ │ ├── package.json │ │ ├── tests │ │ ├── main1.js │ │ └── main2.js │ │ └── very │ │ └── deep │ │ └── tests │ │ ├── deep1.js │ │ └── deeper │ │ └── deep2.js │ └── throws │ ├── cjs │ ├── main.js │ ├── other.js │ ├── package.json │ ├── tests │ │ ├── main1.js │ │ └── main2.js │ └── very │ │ └── deep │ │ └── tests │ │ ├── deep1.js │ │ └── deeper │ │ └── deep2.js │ ├── config.js │ └── esm │ ├── main.js │ ├── other.js │ ├── package.json │ ├── tests │ ├── main1.js │ └── main2.js │ └── very │ └── deep │ └── tests │ ├── deep1.js │ └── deeper │ └── deep2.js ├── test-api-legacy.js ├── test-api-v5.js ├── test-cli.js └── test.html /.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /**/node_modules 3 | /npm-debug.log 4 | /**/.DS_Store 5 | /.eslintcache 6 | 7 | # Ignore this until we find a way to lint them properly 8 | tests/fixtures/**/esm/**/*.js 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "overrides": [ 3 | { 4 | "files": ["ospec.js", "tests/test-api.js"], 5 | "parserOptions": { 6 | "ecmaVersion": 5 7 | } 8 | }, 9 | { 10 | "files": ["tests/fixtures/**/esm/**/*.js"], 11 | "parserOptions" : { 12 | "sourceType": "module" 13 | } 14 | } 15 | ], 16 | "env": { 17 | "browser": true, 18 | "commonjs": true, 19 | "es6": true, 20 | "node": true 21 | }, 22 | "extends": "eslint:recommended", 23 | "parserOptions": { 24 | "ecmaVersion": 2018 25 | }, 26 | "rules": { 27 | "accessor-pairs": "error", 28 | "array-bracket-spacing": [ 29 | "error", 30 | "never" 31 | ], 32 | "array-callback-return": "error", 33 | "arrow-body-style": "error", 34 | "arrow-parens": "error", 35 | "arrow-spacing": "error", 36 | "block-scoped-var": "off", 37 | "block-spacing": "off", 38 | "brace-style": "off", 39 | "callback-return": "off", 40 | "camelcase": [ 41 | "error", 42 | { 43 | "properties": "never" 44 | } 45 | ], 46 | "comma-dangle": [ 47 | "error", 48 | "only-multiline" 49 | ], 50 | "comma-spacing": "off", 51 | "comma-style": [ 52 | "error", 53 | "last" 54 | ], 55 | "complexity": "off", 56 | "computed-property-spacing": [ 57 | "error", 58 | "never" 59 | ], 60 | "consistent-return": "off", 61 | "consistent-this": "off", 62 | "curly": "off", 63 | "default-case": "off", 64 | "dot-location": [ 65 | "error", 66 | "property" 67 | ], 68 | "dot-notation": "off", 69 | "eol-last": "off", 70 | "eqeqeq": "off", 71 | "func-names": "off", 72 | "func-style": "off", 73 | "generator-star-spacing": "error", 74 | "global-require": "error", 75 | "guard-for-in": "off", 76 | "handle-callback-err": "error", 77 | "id-blacklist": "error", 78 | "id-length": "off", 79 | "id-match": "error", 80 | "indent": [ 81 | "warn", 82 | "tab", 83 | { 84 | "outerIIFEBody": 0, 85 | "SwitchCase": 1 86 | } 87 | ], 88 | "init-declarations": "off", 89 | "jsx-quotes": "error", 90 | "key-spacing": "off", 91 | "keyword-spacing": "off", 92 | "linebreak-style": "off", 93 | "lines-around-comment": "off", 94 | "max-depth": "off", 95 | "max-len": "off", 96 | "max-nested-callbacks": "error", 97 | "max-params": "off", 98 | "max-statements": "off", 99 | "max-statements-per-line": "off", 100 | "new-parens": "off", 101 | "newline-after-var": "off", 102 | "newline-before-return": "off", 103 | "newline-per-chained-call": "off", 104 | "no-alert": "error", 105 | "no-array-constructor": "error", 106 | "no-bitwise": "error", 107 | "no-caller": "error", 108 | "no-catch-shadow": "off", 109 | "no-cond-assign": "off", 110 | "no-confusing-arrow": "error", 111 | "no-console": "off", 112 | "no-continue": "off", 113 | "no-div-regex": "error", 114 | "no-duplicate-imports": "error", 115 | "no-else-return": "off", 116 | "no-empty-function": "off", 117 | "no-eq-null": "off", 118 | "no-eval": "error", 119 | "no-extend-native": "off", 120 | "no-extra-bind": "error", 121 | "no-extra-label": "error", 122 | "no-extra-parens": "off", 123 | "no-floating-decimal": "error", 124 | "no-implicit-coercion": "error", 125 | "no-implicit-globals": "error", 126 | "no-implied-eval": "error", 127 | "no-inline-comments": "off", 128 | "no-invalid-this": "off", 129 | "no-iterator": "error", 130 | "no-label-var": "off", 131 | "no-labels": "off", 132 | "no-lone-blocks": "error", 133 | "no-lonely-if": "off", 134 | "no-loop-func": "off", 135 | "no-magic-numbers": "off", 136 | "no-mixed-requires": "error", 137 | "no-multi-spaces": "error", 138 | "no-multi-str": "error", 139 | "no-multiple-empty-lines": "error", 140 | "no-native-reassign": "error", 141 | "no-negated-condition": "off", 142 | "no-nested-ternary": "off", 143 | "no-new": "off", 144 | "no-new-func": "off", 145 | "no-new-object": "error", 146 | "no-new-require": "error", 147 | "no-new-wrappers": "error", 148 | "no-octal-escape": "error", 149 | "no-param-reassign": "off", 150 | "no-path-concat": "off", 151 | "no-plusplus": "off", 152 | "no-process-env": "error", 153 | "no-process-exit": "error", 154 | "no-proto": "error", 155 | "no-redeclare": "off", 156 | "no-restricted-globals": "error", 157 | "no-restricted-imports": "error", 158 | "no-restricted-modules": "error", 159 | "no-restricted-syntax": "error", 160 | "no-return-assign": "off", 161 | "no-script-url": "error", 162 | "no-self-compare": "error", 163 | "no-sequences": "off", 164 | "no-shadow": "off", 165 | "no-shadow-restricted-names": "error", 166 | "no-spaced-func": "error", 167 | "no-sync": "off", 168 | "no-ternary": "off", 169 | "no-throw-literal": "off", 170 | "no-trailing-spaces": [ 171 | "error", 172 | { 173 | "skipBlankLines": true 174 | } 175 | ], 176 | "no-undef-init": "error", 177 | "no-undefined": "off", 178 | "no-underscore-dangle": "off", 179 | "no-unmodified-loop-condition": "error", 180 | "no-unneeded-ternary": "error", 181 | "no-unused-expressions": "off", 182 | "no-use-before-define": "off", 183 | "no-useless-call": "error", 184 | "no-useless-concat": "error", 185 | "no-useless-constructor": "error", 186 | "no-useless-escape": "off", 187 | "no-var": "off", 188 | "no-void": "off", 189 | "no-warning-comments": "off", 190 | "no-whitespace-before-property": "error", 191 | "no-with": "error", 192 | "object-curly-spacing": [ 193 | "error", 194 | "never" 195 | ], 196 | "object-shorthand": "off", 197 | "one-var": "off", 198 | "one-var-declaration-per-line": "off", 199 | "operator-assignment": [ 200 | "error", 201 | "always" 202 | ], 203 | "operator-linebreak": "off", 204 | "padded-blocks": "off", 205 | "prefer-arrow-callback": "off", 206 | "prefer-const": "error", 207 | "prefer-reflect": "off", 208 | "prefer-rest-params": "off", 209 | "prefer-spread": "off", 210 | "prefer-template": "off", 211 | "quote-props": "off", 212 | "quotes": [ 213 | "error", 214 | "double", 215 | {"avoidEscape": true} 216 | ], 217 | "radix": [ 218 | "error", 219 | "always" 220 | ], 221 | "require-jsdoc": "off", 222 | "require-yield": "error", 223 | "semi": "off", 224 | "semi-spacing": "off", 225 | "sort-imports": "error", 226 | "sort-vars": "off", 227 | "space-before-blocks": "off", 228 | "space-before-function-paren": "off", 229 | "space-in-parens": [ 230 | "error", 231 | "never" 232 | ], 233 | "space-infix-ops": "off", 234 | "space-unary-ops": "error", 235 | "spaced-comment": "off", 236 | "strict": ["error", "global"], 237 | "template-curly-spacing": "error", 238 | "valid-jsdoc": "off", 239 | "vars-on-top": "off", 240 | "wrap-iife": "off", 241 | "wrap-regex": "error", 242 | "yield-star-spacing": "error", 243 | "yoda": "off" 244 | }, 245 | "root": true 246 | }; 247 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | /mithril.js binary 3 | /mithril.min.js binary 4 | /package-lock.json binary 5 | /yarn.lock binary 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @MithrilJS/Committers 2 | /.github/ @MithrilJS/Admins 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | name: ${{ matrix.task }}, node v${{ matrix.node_version }} on ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | task: 12 | - test-cli 13 | - test-api 14 | os: 15 | - ubuntu-latest 16 | - macos-latest 17 | - windows-latest 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | node_version: 20 | - 16 21 | - 18 22 | - 20 23 | - 22 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Use Node.js ${{ matrix.node_version }} 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node_version }} 30 | - run: npm ci 31 | - run: npm run ${{ matrix.task }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /**/node_modules 3 | /jsconfig.json 4 | /npm-debug.log 5 | /.vscode 6 | /.DS_Store 7 | /.eslintcache 8 | /tests/**/package-lock.json 9 | # These are artifacts from various scripts 10 | /dist 11 | /archive 12 | /logs 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Leo Horie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README v5 preview.md: -------------------------------------------------------------------------------- 1 | # ospec 2 | 3 | [![npm License](https://img.shields.io/npm/l/ospec.svg)](https://www.npmjs.com/package/ospec) [![npm Version](https://img.shields.io/npm/v/ospec.svg)](https://www.npmjs.com/package/ospec) ![Build Status](https://img.shields.io/github/actions/workflow/status/MithrilJS/ospec/.github%2Fworkflows%2Fci.yml) [![npm Downloads](https://img.shields.io/npm/dm/ospec.svg)](https://www.npmjs.com/package/ospec) 4 | 5 | [![Donate at OpenCollective](https://img.shields.io/opencollective/all/mithriljs.svg?colorB=brightgreen)](https://opencollective.com/mithriljs) [![Zulip, join chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://mithril.zulipchat.com/) 6 | 7 | --- 8 | 9 | [About](#about) | [Usage](#usage) | [CLI](#command-line-interface) | [API](#api) | [Goals](#goals) 10 | 11 | Noiseless testing framework 12 | 13 | ## About 14 | 15 | - ~660 LOC including the CLI runner 16 | - terser and faster test code than with mocha, jasmine or tape 17 | - test code reads like bullet points 18 | - assertion code follows [SVO](https://en.wikipedia.org/wiki/Subject–verb–object) structure in present tense for terseness and readability 19 | - supports: 20 | - test grouping 21 | - assertions 22 | - spies 23 | - `equals`, `notEquals`, `deepEquals` and `notDeepEquals` assertion types 24 | - `before`/`after`/`beforeEach`/`afterEach` hooks 25 | - test exclusivity (i.e. `.only`) 26 | - async tests and hooks 27 | - explicitly regulates test-space configuration to encourage focus on testing, and to provide uniform test suites across projects 28 | 29 | ## Usage 30 | 31 | ### Single tests 32 | 33 | Both tests and assertions are declared via the `o` function. Tests should have a description and a body function. A test may have one or more assertions. Assertions should appear inside a test's body function and compare two values. 34 | 35 | ```javascript 36 | var o = require("ospec") 37 | 38 | o("addition", o => { 39 | o(1 + 1).equals(2) 40 | }) 41 | o("subtraction", o => { 42 | o(1 - 1).notEquals(2) 43 | }) 44 | ``` 45 | 46 | Assertions may have descriptions: 47 | 48 | ```javascript 49 | o("addition", o => { 50 | o(1 + 1).equals(2)("addition should work") 51 | 52 | /* in ES6, the following syntax is also possible 53 | o(1 + 1).equals(2) `addition should work` 54 | */ 55 | }) 56 | /* for a failing test, an assertion with a description outputs this: 57 | 58 | addition should work 59 | 60 | 1 should equal 2 61 | 62 | Error 63 | at stacktrace/goes/here.js:1:1 64 | */ 65 | ``` 66 | 67 | ### Grouping tests 68 | 69 | Tests may be organized into logical groups using `o.spec` 70 | 71 | ```javascript 72 | o.spec("math", () => { 73 | o("addition", o => { 74 | o(1 + 1).equals(2) 75 | }) 76 | o("subtraction", o => { 77 | o(1 - 1).notEquals(2) 78 | }) 79 | }) 80 | ``` 81 | 82 | Group names appear as a breadcrumb trail in test descriptions: `math > addition: 2 should equal 2` 83 | 84 | ### Nested test groups 85 | 86 | Groups can be nested to further organize test groups. Note that tests cannot be nested inside other tests. 87 | 88 | ```javascript 89 | o.spec("math", () => { 90 | o.spec("arithmetics", () => { 91 | o("addition", o => { 92 | o(1 + 1).equals(2) 93 | }) 94 | o("subtraction", o => { 95 | o(1 - 1).notEquals(2) 96 | }) 97 | }) 98 | }) 99 | ``` 100 | 101 | ### Callback test 102 | 103 | The `o.spy()` method can be used to create a stub function that keeps track of its call count and received parameters 104 | 105 | ```javascript 106 | //code to be tested 107 | function call(cb, arg) {cb(arg)} 108 | 109 | //test suite 110 | var o = require("ospec") 111 | 112 | o.spec("call()", () => { 113 | o("works", o => { 114 | var spy = o.spy() 115 | call(spy, 1) 116 | 117 | o(spy.callCount).equals(1) 118 | o(spy.args[0]).equals(1) 119 | o(spy.calls[0]).deepEquals([1]) 120 | }) 121 | }) 122 | ``` 123 | 124 | A spy can also wrap other functions, like a decorator: 125 | 126 | ```javascript 127 | //code to be tested 128 | var count = 0 129 | function inc() { 130 | count++ 131 | } 132 | 133 | //test suite 134 | var o = require("ospec") 135 | 136 | o.spec("call()", () => { 137 | o("works", o => { 138 | var spy = o.spy(inc) 139 | spy() 140 | 141 | o(count).equals(spy.callCount) 142 | }) 143 | }) 144 | 145 | ``` 146 | 147 | ### Asynchronous tests 148 | 149 | ```javascript 150 | o("setTimeout calls callback", o => { 151 | return new Promise(fulfill => setTimeout(fulfill, 10)) 152 | }) 153 | ``` 154 | 155 | Alternativly you can return a promise or even use an async function in tests: 156 | 157 | ```javascript 158 | o("promise test", o => { 159 | return new Promise(resolve => { 160 | setTimeout(resolve, 10) 161 | }) 162 | }) 163 | ``` 164 | 165 | ```javascript 166 | o("promise test", async () => { 167 | await someOtherAsyncFunction() 168 | }) 169 | ``` 170 | 171 | #### Timeout delays 172 | 173 | By default, asynchronous tests time out after 200ms. You can change that default for the current test suite and 174 | its children by using the `o.specTimeout(delay)` function. 175 | 176 | ```javascript 177 | o.spec("a spec that must timeout quickly", () => { 178 | // wait 20ms before bailing out of the tests of this suite and 179 | // its descendants 180 | const waitFor = n => new Promise(f => setTimeout(f, n)) 181 | o.specTimeout(20) 182 | o("some test", async o => { 183 | await waitFor(10) 184 | o(1 + 1).equals(2) 185 | }) 186 | 187 | o.spec("a child suite where the delay also applies", () => { 188 | o("some test", async o => { 189 | await waitFor(30) // this will cause a timeout to be reported 190 | o(1 + 1).equals(2)// even if the assertions succeed. 191 | }) 192 | }) 193 | }) 194 | o.spec("a spec that uses the default delay", () => { 195 | // ... 196 | }) 197 | ``` 198 | 199 | This can also be changed on a per-test basis using the `o.timeout(delay)` function from within a test: 200 | 201 | ```javascript 202 | const waitFor = n => new Promise(f => setTimeout(f, n)) 203 | o("setTimeout calls callback", async o => { 204 | o.timeout(500) //wait 500ms before setting the test as timed out and moving forward. 205 | await(300) 206 | o(1 + 1).equals(2) 207 | }) 208 | ``` 209 | 210 | Note that the `o.timeout` function call must be the first statement in its test. 211 | 212 | Test timeouts are reported along with test failures and errors thrown at the end of the run. A test timeout causes the test runner to exit with a non-zero status code. 213 | 214 | ### `before`, `after`, `beforeEach`, `afterEach` hooks 215 | 216 | These hooks can be declared when it's necessary to setup and clean up state for a test or group of tests. The `before` and `after` hooks run once each per test group, whereas the `beforeEach` and `afterEach` hooks run for every test. 217 | 218 | ```javascript 219 | o.spec("math", () => { 220 | var acc 221 | o.beforeEach(() => { 222 | acc = 0 223 | }) 224 | 225 | o("addition", o => { 226 | acc += 1 227 | 228 | o(acc).equals(1) 229 | }) 230 | o("subtraction", o => { 231 | acc -= 1 232 | 233 | o(acc).equals(-1) 234 | }) 235 | }) 236 | ``` 237 | 238 | It's strongly recommended to ensure that `beforeEach` hooks always overwrite all shared variables, and avoid `if/else` logic, memoization, undo routines inside `beforeEach` hooks. 239 | 240 | You can run assertions from the hooks: 241 | 242 | ```javascript 243 | o.afterEach(o => { 244 | o(postConditions).equals(met) 245 | }) 246 | ``` 247 | 248 | ### Asynchronous hooks 249 | 250 | Like tests, hooks can also be asynchronous. Tests that are affected by asynchronous hooks will wait for the hooks to complete before running. 251 | 252 | ```javascript 253 | o.spec("math", () => { 254 | let state 255 | o.beforeEach(async() => { 256 | // async initialization 257 | state = await (async function () {return 0})() 258 | }) 259 | 260 | //tests only run after the async hooks are complete 261 | o("addition", o => { 262 | state += 1 263 | 264 | o(state).equals(1) 265 | }) 266 | o("subtraction", o => { 267 | acc -= 1 268 | 269 | o(state).equals(-1) 270 | }) 271 | }) 272 | ``` 273 | 274 | To ease the transition from older `ospec` versions to the v5+ API, we also provide a `done` helper, to be used as follow: 275 | 276 | ```javascript 277 | o("setTimeout calls callback", ({o, done}) => { 278 | setTimeout(()=>{ 279 | if (error) done(error) 280 | else done() 281 | }), 10) 282 | }) 283 | ``` 284 | 285 | If an argument is passed to `done`, the corresponding promise is rejected. 286 | 287 | ### Running only some tests 288 | 289 | One or more tests can be temporarily made to run exclusively by calling `o.only()` instead of `o`. This is useful when troubleshooting regressions, to zero-in on a failing test, and to avoid saturating console log w/ irrelevant debug information. 290 | 291 | ```javascript 292 | o.spec("math", () => { 293 | // will not run 294 | o("addition", o => { 295 | o(1 + 1).equals(2) 296 | }) 297 | 298 | // this test will be run, regardless of how many groups there are 299 | o.only("subtraction", () => { 300 | o(1 - 1).notEquals(2) 301 | }) 302 | 303 | // will not run 304 | o("multiplication", o => { 305 | o(2 * 2).equals(4) 306 | }) 307 | 308 | // this test will be run, regardless of how many groups there are 309 | o.only("division", () => { 310 | o(6 / 2).notEquals(2) 311 | }) 312 | }) 313 | ``` 314 | 315 | ### Running the test suite 316 | 317 | ```javascript 318 | //define a test 319 | o("addition", o => { 320 | o(1 + 1).equals(2) 321 | }) 322 | 323 | //run the suite 324 | o.run() 325 | ``` 326 | 327 | ### Running test suites concurrently 328 | 329 | The `o.new()` method can be used to create new instances of ospec, which can be run in parallel. Note that each instance will report independently, and there's no aggregation of results. 330 | 331 | ```javascript 332 | var _o = o.new('optional name') 333 | _o("a test", o => { 334 | o(1).equals(1) 335 | }) 336 | _o.run() 337 | ``` 338 | 339 | ## Command Line Interface 340 | 341 | Create a script in your package.json: 342 | 343 | ```javascript 344 | "scripts": { 345 | "test": "ospec", 346 | ... 347 | } 348 | ``` 349 | 350 | ...and run it from the command line: 351 | 352 | ```shell 353 | npm test 354 | ``` 355 | 356 | **NOTE:** `o.run()` is automatically called by the CLI runner - no need to call it in your test code. 357 | 358 | ### CLI Options 359 | 360 | Running ospec without arguments is equivalent to running `ospec '**/tests/**/*.js'`. In english, this tells ospec to evaluate all `*.js` files in any sub-folder named `tests/` (the `node_modules` folder is always excluded). 361 | 362 | If you wish to change this behavior, just provide one or more glob match patterns: 363 | 364 | ```shell 365 | ospec '**/spec/**/*.js' '**/*.spec.js' 366 | ``` 367 | 368 | You can also provide ignore patterns (note: always add `--ignore` AFTER match patterns): 369 | 370 | ```shell 371 | ospec --ignore 'folder1/**' 'folder2/**' 372 | ``` 373 | 374 | Finally, you may choose to load files or modules before any tests run (**note:** always add `--preload` AFTER match patterns): 375 | 376 | ```shell 377 | ospec --preload esm 378 | ``` 379 | 380 | Here's an example of mixing them all together: 381 | 382 | ```shell 383 | ospec '**/*.test.js' --ignore 'folder1/**' --preload esm ./my-file.js 384 | ``` 385 | 386 | ### native mjs and module support 387 | 388 | For Node.js versions >= 13.2, `ospec` supports both ES6 modules and CommonJS packages out of the box. `--preload esm` is thus not needed in that case. 389 | 390 | ### Run ospec directly from the command line 391 | 392 | ospec comes with an executable named `ospec`. npm auto-installs local binaries to `./node_modules/.bin/`. You can run ospec by running `./node_modules/.bin/ospec` from your project root, but there are more convenient methods to do so that we will soon describe. 393 | 394 | ospec doesn't work when installed globally (`npm install -g`). Using global scripts is generally a bad idea since you can end up with different, incompatible versions of the same package installed locally and globally. 395 | 396 | Here are different ways of running ospec from the command line. This knowledge applies to not just ospec, but any locally installed npm binary. 397 | 398 | #### npx 399 | 400 | If you're using a recent version of npm (v5+), you can use run `npx ospec` from your project folder. 401 | 402 | #### npm-run 403 | 404 | If you're using a recent version of npm (v5+), you can use run `npx ospec` from your project folder. 405 | 406 | Otherwise, to work around this limitation, you can use [`npm-run`](https://www.npmjs.com/package/npm-run) which enables one to run the binaries of locally installed packages. 407 | 408 | ```shell 409 | npm install npm-run -g 410 | ``` 411 | 412 | Then, from a project that has ospec installed as a (dev) dependency: 413 | 414 | ```shell 415 | npm-run ospec 416 | ``` 417 | 418 | #### PATH 419 | 420 | If you understand how your system's PATH works (e.g. for [OSX](https://coolestguidesontheplanet.com/add-shell-path-osx/)), then you can add the following to your PATH... 421 | 422 | ```shell 423 | export PATH=./node_modules/.bin:$PATH 424 | ``` 425 | 426 | ...and you'll be able to run `ospec` without npx, npm, etc. This one-time setup will also work with other binaries across all your node projects, as long as you run binaries from the root of your projects. 427 | 428 | --- 429 | 430 | ## API 431 | 432 | Square brackets denote optional arguments 433 | 434 | ### `o.spec(title: string, tests: () => void) => void` 435 | 436 | Defines a group of tests. Groups are optional 437 | 438 | --- 439 | 440 | ### `o(title: string, assertions: (o: AssertionFactory) => void) => void` 441 | 442 | Defines a test. The `assertions` function can be async. It receives the assertion factory as argument. 443 | 444 | --- 445 | 446 | ### `type AssertionFactory = (value: any) => Assertion` 447 | 448 | Starts an assertion. There are seven types of assertion: `equals`, `notEquals`, `deepEquals`, `notDeepEquals`, `throws`, `notThrows`, and, for extensions, `_`. 449 | 450 | ```typescript 451 | type OptionalMessage = (message:string) => void 452 | 453 | type AssertionResult = {pass: boolean, message: string} 454 | 455 | interface Assertion { 456 | equals: (value: any) => OptionalMessage 457 | notEquals: (value: any) => OptionalMessage 458 | deepEquals: (value: any) => OptionalMessage 459 | notDeepEquals: (value: any) => OptionalMessage 460 | throws: (value: any) => OptionalMessage 461 | notThrows: (value: any) => OptionalMessage 462 | // For plugins: 463 | _: (validator: ()=>AssertionResult) => void 464 | } 465 | 466 | ``` 467 | 468 | Assertions have this form: 469 | 470 | ```javascript 471 | o(actualValue).equals(expectedValue) 472 | ``` 473 | 474 | As a matter of convention, the actual value should be the first argument and the expected value should be the second argument in an assertion. 475 | 476 | Assertions can also accept an optional description curried parameter: 477 | 478 | ```javascript 479 | o(actualValue).equals(expectedValue)("this is a description for this assertion") 480 | ``` 481 | 482 | Assertion descriptions can be simplified using ES6 tagged template string syntax: 483 | 484 | ```javascript 485 | o(actualValue).equals(expectedValue)`likewise, with an interpolated ${value}` 486 | ``` 487 | 488 | #### `o(value: any).equals(value: any)` 489 | 490 | Asserts that two values are strictly equal (`===`). Returns an `OptionalMessage` that can be called if desired to contextualize the assertion message. 491 | 492 | #### `o(value: any).notEquals(value: any)` 493 | 494 | Asserts that two values are strictly not equal (`!==`). Returns an `OptionalMessage` that can be called if desired to contextualize the assertion message. 495 | 496 | #### `o(value: any).deepEquals(value: any)` 497 | 498 | Asserts that two values are recursively equal. Returns an `OptionalMessage` that can be called if desired to contextualize the assertion message. 499 | 500 | #### `o(value: any).notDeepEquals(value: any)` 501 | 502 | Asserts that two values are not recursively equal. Returns an `OptionalMessage` that can be called if desired to contextualize the assertion message. 503 | 504 | #### `o(fn: (...args: any[]) => any).throws(fn: constructor)` 505 | 506 | Asserts that a function throws an instance of the provided constructor. Returns an `OptionalMessage` that can be called if desired to contextualize the assertion message. 507 | 508 | #### `o(fn: (...args: any[]) => any).throws(message: string)` 509 | 510 | Asserts that a function throws an Error with the provided message. Returns an `OptionalMessage` that can be called if desired to contextualize the assertion message. 511 | 512 | #### `o(fn: (...args: any[]) => any).notThrows(fn: constructor)` 513 | 514 | Asserts that a function does not throw an instance of the provided constructor. Returns an `OptionalMessage` that can be called if desired to contextualize the assertion message. 515 | 516 | #### `o(fn: (...args: any[]) => any).notThrows(message: string)` 517 | 518 | Asserts that a function does not throw an Error with the provided message. Returns an `OptionalMessage` that can be called if desired to contextualize the assertion message. 519 | 520 | --- 521 | 522 | ### `o.before(setup: (o: AssertionFactoy) => void)` 523 | 524 | Defines code to be run at the beginning of a test group. 525 | 526 | The `AssertionFactory` is injected as an argument into the `setup` function. It is called `o` by convention. 527 | 528 | --- 529 | 530 | ### `o.after(teardown: (o: AssertionFactoy) => void)` 531 | 532 | Defines code to be run at the end of a test group. 533 | 534 | The `AssertionFactory` is injected as an argument into the `setup` function. It is called `o` by convention. 535 | 536 | --- 537 | 538 | ### `o.beforeEach(setup: (o: AssertionFactoy) => void)` 539 | 540 | Defines code to be run before each test in a group. 541 | 542 | The `AssertionFactory` is injected as an argument into the `setup` function. It is called `o` by convention. 543 | 544 | --- 545 | 546 | ### `o.after(teardown: (o: AssertionFactoy) => void)` 547 | 548 | Defines code to be run after each test in a group. 549 | 550 | The `AssertionFactory` is injected as an argument into the `setup` function. It is called `o` by convention. 551 | 552 | --- 553 | 554 | ### `o.only(title: string, assertions: (o: AssertionFactoy) => void)` 555 | 556 | You can replace a `o("message", o=>{/* assertions */})` call with `o.only("message", o=>{/* assertions */})`. If `o.only` is encountered plain `o()` test definitions will be ignored, and those maked as `only` will be the only ones to run. 557 | 558 | --- 559 | 560 | ### `o.spy(fn: (...args: any[]) => any)` 561 | 562 | Returns a function that records the number of times it gets called, and its arguments. 563 | 564 | The resulting function has the same `.name` and `.length` properties as the one `o.spy()` received as argument. It also has the following additional properties: 565 | 566 | #### `o.spy().callCount` 567 | 568 | The number of times the function has been called 569 | 570 | #### `o.spy().args` 571 | 572 | The `arguments` that were passed to the function in the last time it was called 573 | 574 | #### `o.spy().calls` 575 | 576 | An array of `{this, args}` objects that reflect, for each time the spied on function was called, the `this` value recieved if it was called as a method, and the corresponding `args`. 577 | 578 | --- 579 | 580 | ### `o.run(reporter: (results: Result[]) => number)` 581 | 582 | Runs the test suite. By default passing test results are printed using 583 | `console.log` and failing test results are printed using `console.error`. 584 | 585 | If you have custom continuous integration needs then you can use a 586 | reporter to process [test result data](#result-data) yourself. 587 | 588 | If running in Node.js, ospec will call `process.exit` after reporting 589 | results by default. If you specify a reporter, ospec will not do this 590 | and allows your reporter to respond to results in its own way. 591 | 592 | --- 593 | 594 | ### `o.report(results: Result[])` 595 | 596 | The default reporter used by `o.run()` when none are provided. Returns the number of failures. It expects an array of [test result data](#result-data) as argument. 597 | 598 | --- 599 | 600 | ### `o.new()` 601 | 602 | Returns a new instance of ospec. Useful if you want to run more than one test suite concurrently 603 | 604 | ```javascript 605 | var $o = o.new() 606 | $o("a test", o => { 607 | o(1).equals(1) 608 | }) 609 | $o.run() 610 | ``` 611 | 612 | ### throwing Errors 613 | 614 | When an error is thrown some tests may be skipped. See the "run time model" for a detailed description of the bailout mechanism. 615 | 616 | --- 617 | 618 | ## Result data 619 | 620 | Test results are available by reference for integration purposes. You 621 | can use custom reporters in `o.run()` to process these results. 622 | 623 | ```javascript 624 | interface Result { 625 | pass: Boolean | null, 626 | message: string, 627 | context: string, 628 | error: Error, 629 | testError: Error, 630 | } 631 | 632 | o.run((results: Results[]) => { 633 | // results is an array 634 | 635 | results.forEach(result => { 636 | // ... 637 | }) 638 | }) 639 | ``` 640 | 641 | --- 642 | 643 | ### `result.pass` 644 | 645 | - `true` if the assertion passed. 646 | - `false` if the assertion failed. 647 | - `null` if the assertion was incomplete (`o("partial assertion")` without an assertion method called). 648 | 649 | --- 650 | 651 | ### `result.error` 652 | 653 | The `Error` object explaining the reason behind a failure. If the assertion failed, the stack will point to the actuall error. If the assertion did pass or was incomplete, this field is identical to `result.testError`. 654 | 655 | --- 656 | 657 | ### `result.testError` 658 | 659 | An `Error` object whose stack points to the test definition that wraps the assertion. Useful as a fallback because in some async cases the main may not point to test code. 660 | 661 | --- 662 | 663 | ### `result.message` 664 | 665 | If an exception was thrown inside the corresponding test, this will equal that Error's `message`. Otherwise, this will be a preformatted message in [SVO form](https://en.wikipedia.org/wiki/Subject%E2%80%93verb%E2%80%93object). More specifically, `${subject}\n${verb}\n${object}`. 666 | 667 | As an example, the following test's result message will be `"false\nshould equal\ntrue"`. 668 | 669 | ```javascript 670 | o.spec("message", () => { 671 | o(false).equals(true) 672 | }) 673 | ``` 674 | 675 | If you specify an assertion description, that description will appear two lines above the subject. 676 | 677 | ```javascript 678 | o.spec("message", () => { 679 | o(false).equals(true)("Candyland") // result.message === "Candyland\n\nfalse\nshould equal\ntrue" 680 | }) 681 | ``` 682 | 683 | --- 684 | 685 | ### `result.context` 686 | 687 | A `>`-separated string showing the structure of the test specification. 688 | In the below example, `result.context` would be `testing > rocks`. 689 | 690 | ```javascript 691 | o.spec("testing", () => { 692 | o.spec("rocks", () => { 693 | o(false).equals(true) 694 | }) 695 | }) 696 | ``` 697 | 698 | --- 699 | 700 | ## Run time model 701 | 702 | ### Definitions 703 | 704 | - A **test** is the function passed to `o("description", function test() {})`. 705 | - A **hook** is a function passed to `o.before()`, `o.after()`. `o.beforeEach()` and `o.afterEach()`. 706 | - A **task** designates either a test or a hook. 707 | - A given test and its associated `beforeEach` and `afterEach` hooks form a **streak**. The `beforeEach` hooks run outermost first, the `afterEach` run outermost last. The hooks are optional, and are tied at test-definition time in the `o.spec()` calls that enclose the test. 708 | - A **spec** is a collection of streaks, specs, one `before` hook and one `after` hook. Each component is optional. Specs are defined with the `o.spec("spec name", function specDef() {})` calls. 709 | 710 | ### The phases of an ospec run 711 | 712 | For a given instance, an `ospec` run goes through three phases: 713 | 714 | 1) tests definition 715 | 1) tests execution and results accumulation 716 | 1) results presentation 717 | 718 | #### Tests definition 719 | 720 | This phase is synchronous. `o.spec("spec name", function specDef() {})`, `o("test name", function test() {})` and hooks calls generate a tree of specs and tests. 721 | 722 | #### Test execution and results accumulation 723 | 724 | At test execution time, for each spec, the `before` hook is called if present, then nested specs the streak of each test, in definition order, then the `after` hook, if present. 725 | 726 | Test and hooks may contain assertions, which will populate the `results` array. 727 | 728 | #### Results presentation 729 | 730 | Once all tests have run or timed out, the results are presented. 731 | 732 | ### Throwing errors and spec bail out 733 | 734 | While some testing libraries consider error thrown as assertions failure, `ospec` treats them as super-failures. Throwing will cause the current spec to be aborted, avoiding what can otherwise end up as pages of errors. What this means depends on when the error is thrown. Specifically: 735 | 736 | - A syntax error in a file causes the file to be ignored by the runner. 737 | - At test-definition time: 738 | - An error thrown at the root of a file will cause subsequent tests and specs to be ignored. 739 | - An error thrown in a spec definition will cause the spec to be ignored. 740 | - At test-execution time: 741 | - An error thrown in the `before` hook will cause the streaks and nested specs to be ignored. The `after` hook will run. 742 | - An error thrown in a task... 743 | - ...prevents further streaks and nested specs in the current spec from running. The `after` *hook* of the spec will run. 744 | - ...if thrown in a `beforeEach` hook of a streak, causes the streak to be hollowed out. Hooks defined in nested scopes and the actual test will not run. However, the `afterEach` hook corresponding to the one that crashed will run, as will those defined in outer scopes. 745 | 746 | For every error thrown, a "bail out" failure is reported. 747 | 748 | --- 749 | 750 | ## Goals 751 | 752 | Ospec started as a bare bones test runner optimized for Leo Horie to write Mithril v1 with as little hasle as possible. It has since grown in capabilities and polish, and while we tried to keep some of the original spirit, the current incarnation is not as radically minimalist as the original. The state of the art in testing has also moved with the dominance of Jest over Jasmine and Mocha, and now Vitest coming up the horizon. The goals in 2023 are: 753 | 754 | - Do the most common things that the mocha/chai/sinon triad does without having to install 3 different libraries and several dozen dependencies 755 | - Limit configuration in test-space: 756 | - Disallow ability to pick between API styles (BDD/TDD/Qunit, assert/should/expect, etc) 757 | - No "magic" plugin system with global reach. 758 | - Provide a default simple reporter 759 | - Make assertion code terse, readable and self-descriptive 760 | - Have as few assertion types as possible for a workable usage pattern 761 | - Don't flood the result log with failures if you break a core part of the project you're testing. An error thrown in test space will abort the current spec. 762 | 763 | These restrictions have a few benefits: 764 | 765 | - tests always look the same, even across different projects and teams 766 | - single source of documentation for entire testing API 767 | - no need to hunt down plugins to figure out what they do, especially if they replace common javascript idioms with fuzzy spoken language constructs (e.g. what does `.is()` do?) 768 | - no need to pollute project-space with ad-hoc configuration code 769 | - discourages side-tracking and yak-shaving 770 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ospec 2 | 3 | [![npm License](https://img.shields.io/npm/l/ospec.svg)](https://www.npmjs.com/package/ospec) [![npm Version](https://img.shields.io/npm/v/ospec.svg)](https://www.npmjs.com/package/ospec) ![Build Status](https://img.shields.io/github/actions/workflow/status/MithrilJS/ospec/.github%2Fworkflows%2Fci.yml) [![npm Downloads](https://img.shields.io/npm/dm/ospec.svg)](https://www.npmjs.com/package/ospec) 4 | 5 | [![Donate at OpenCollective](https://img.shields.io/opencollective/all/mithriljs.svg?colorB=brightgreen)](https://opencollective.com/mithriljs) [![Zulip, join chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://mithril.zulipchat.com/) 6 | 7 | --- 8 | 9 | [About](#about) | [Usage](#usage) | [CLI](#command-line-interface) | [API](#api) | [Goals](#goals) 10 | 11 | Noiseless testing framework 12 | 13 | ## About 14 | 15 | - ~950 LOC including the CLI runner1 16 | - terser and faster test code than with mocha, jasmine or tape 17 | - test code reads like bullet points 18 | - assertion code follows [SVO](https://en.wikipedia.org/wiki/Subject–verb–object) structure in present tense for terseness and readability 19 | - supports: 20 | - test grouping 21 | - assertions 22 | - spies 23 | - `equals`, `notEquals`, `deepEquals` and `notDeepEquals` assertion types 24 | - `before`/`after`/`beforeEach`/`afterEach` hooks 25 | - test exclusivity (i.e. `.only`) 26 | - async tests and hooks 27 | - explicitly regulates test-space configuration to encourage focus on testing, and to provide uniform test suites across projects 28 | 29 | Note: 1 ospec is currently in the process of changing some of its API surface. The legacy and updated APIs are both implemented right now to ease the transition, once legacy code has been removed we'll clock around 800 LOC. 30 | 31 | ## Usage 32 | 33 | ### Single tests 34 | 35 | Both tests and assertions are declared via the `o` function. Tests should have a description and a body function. A test may have one or more assertions. Assertions should appear inside a test's body function and compare two values. 36 | 37 | ```javascript 38 | var o = require("ospec") 39 | 40 | o("addition", function() { 41 | o(1 + 1).equals(2) 42 | }) 43 | o("subtraction", function() { 44 | o(1 - 1).notEquals(2) 45 | }) 46 | ``` 47 | 48 | Assertions may have descriptions: 49 | 50 | ```javascript 51 | o("addition", function() { 52 | o(1 + 1).equals(2)("addition should work") 53 | 54 | /* in ES6, the following syntax is also possible 55 | o(1 + 1).equals(2) `addition should work` 56 | */ 57 | }) 58 | /* for a failing test, an assertion with a description outputs this: 59 | 60 | addition should work 61 | 62 | 1 should equal 2 63 | 64 | Error 65 | at stacktrace/goes/here.js:1:1 66 | */ 67 | ``` 68 | 69 | ### Grouping tests 70 | 71 | Tests may be organized into logical groups using `o.spec` 72 | 73 | ```javascript 74 | o.spec("math", function() { 75 | o("addition", function() { 76 | o(1 + 1).equals(2) 77 | }) 78 | o("subtraction", function() { 79 | o(1 - 1).notEquals(2) 80 | }) 81 | }) 82 | ``` 83 | 84 | Group names appear as a breadcrumb trail in test descriptions: `math > addition: 2 should equal 2` 85 | 86 | ### Nested test groups 87 | 88 | Groups can be nested to further organize test groups. Note that tests cannot be nested inside other tests. 89 | 90 | ```javascript 91 | o.spec("math", function() { 92 | o.spec("arithmetics", function() { 93 | o("addition", function() { 94 | o(1 + 1).equals(2) 95 | }) 96 | o("subtraction", function() { 97 | o(1 - 1).notEquals(2) 98 | }) 99 | }) 100 | }) 101 | ``` 102 | 103 | ### Callback test 104 | 105 | The `o.spy()` method can be used to create a stub function that keeps track of its call count and received parameters 106 | 107 | ```javascript 108 | //code to be tested 109 | function call(cb, arg) {cb(arg)} 110 | 111 | //test suite 112 | var o = require("ospec") 113 | 114 | o.spec("call()", function() { 115 | o("works", function() { 116 | var spy = o.spy() 117 | call(spy, 1) 118 | 119 | o(spy.callCount).equals(1) 120 | o(spy.args[0]).equals(1) 121 | o(spy.calls[0].args).deepEquals([1]) 122 | }) 123 | }) 124 | ``` 125 | 126 | A spy can also wrap other functions, like a decorator: 127 | 128 | ```javascript 129 | //code to be tested 130 | var count = 0 131 | function inc() { 132 | count++ 133 | } 134 | 135 | //test suite 136 | var o = require("ospec") 137 | 138 | o.spec("call()", function() { 139 | o("works", function() { 140 | var spy = o.spy(inc) 141 | spy() 142 | 143 | o(count).equals(1) 144 | }) 145 | }) 146 | 147 | ``` 148 | 149 | ### Asynchronous tests 150 | 151 | If a test body function declares a named argument, the test is assumed to be asynchronous, and the argument is a function that must be called exactly one time to signal that the test has completed. As a matter of convention, this argument is typically named `done`. 152 | 153 | ```javascript 154 | o("setTimeout calls callback", function(done) { 155 | setTimeout(done, 10) 156 | }) 157 | ``` 158 | 159 | Alternatively you can return a promise or even use an async function in tests: 160 | 161 | ```javascript 162 | o("promise test", function() { 163 | return new Promise(function(resolve) { 164 | setTimeout(resolve, 10) 165 | }) 166 | }) 167 | ``` 168 | 169 | ```javascript 170 | o("promise test", async function() { 171 | await someOtherAsyncFunction() 172 | }) 173 | ``` 174 | 175 | #### Timeout delays 176 | 177 | By default, asynchronous tests time out after 200ms. You can change that default for the current test suite and 178 | its children by using the `o.specTimeout(delay)` function. 179 | 180 | ```javascript 181 | o.spec("a spec that must timeout quickly", function() { 182 | // wait 20ms before bailing out of the tests of this suite and 183 | // its descendants 184 | o.specTimeout(20) 185 | o("some test", function(done) { 186 | setTimeout(done, 10) // this will pass 187 | }) 188 | 189 | o.spec("a child suite where the delay also applies", function () { 190 | o("some test", function(done) { 191 | setTimeout(done, 30) // this will time out. 192 | }) 193 | }) 194 | }) 195 | o.spec("a spec that uses the default delay", function() { 196 | // ... 197 | }) 198 | ``` 199 | 200 | This can also be changed on a per-test basis using the `o.timeout(delay)` function from within a test: 201 | 202 | ```javascript 203 | o("setTimeout calls callback", function(done) { 204 | o.timeout(500) //wait 500ms before bailing out of the test 205 | 206 | setTimeout(done, 300) 207 | }) 208 | ``` 209 | 210 | Note that the `o.timeout` function call must be the first statement in its test. It also works with Promise-returning tests: 211 | 212 | ```javascript 213 | o("promise test", function() { 214 | o.timeout(1000) 215 | return someOtherAsyncFunctionThatTakes900ms() 216 | }) 217 | ``` 218 | 219 | ```javascript 220 | o("promise test", async function() { 221 | o.timeout(1000) 222 | await someOtherAsyncFunctionThatTakes900ms() 223 | }) 224 | ``` 225 | 226 | Asynchronous tests generate an assertion that succeeds upon calling `done` or fails on timeout with the error message `async test timed out`. 227 | 228 | ### `before`, `after`, `beforeEach`, `afterEach` hooks 229 | 230 | These hooks can be declared when it's necessary to setup and clean up state for a test or group of tests. The `before` and `after` hooks run once each per test group, whereas the `beforeEach` and `afterEach` hooks run for every test. 231 | 232 | ```javascript 233 | o.spec("math", function() { 234 | var acc 235 | o.beforeEach(function() { 236 | acc = 0 237 | }) 238 | 239 | o("addition", function() { 240 | acc += 1 241 | 242 | o(acc).equals(1) 243 | }) 244 | o("subtraction", function() { 245 | acc -= 1 246 | 247 | o(acc).equals(-1) 248 | }) 249 | }) 250 | ``` 251 | 252 | It's strongly recommended to ensure that `beforeEach` hooks always overwrite all shared variables, and avoid `if/else` logic, memoization, undo routines inside `beforeEach` hooks. 253 | 254 | ### Asynchronous hooks 255 | 256 | Like tests, hooks can also be asynchronous. Tests that are affected by asynchronous hooks will wait for the hooks to complete before running. 257 | 258 | ```javascript 259 | o.spec("math", function() { 260 | var acc 261 | o.beforeEach(function(done) { 262 | setTimeout(function() { 263 | acc = 0 264 | done() 265 | }) 266 | }) 267 | 268 | //tests only run after async hooks complete 269 | o("addition", function() { 270 | acc += 1 271 | 272 | o(acc).equals(1) 273 | }) 274 | o("subtraction", function() { 275 | acc -= 1 276 | 277 | o(acc).equals(-1) 278 | }) 279 | }) 280 | ``` 281 | 282 | ### Running only some tests 283 | 284 | One or more tests can be temporarily made to run exclusively by calling `o.only()` instead of `o`. This is useful when troubleshooting regressions, to zero-in on a failing test, and to avoid saturating console log w/ irrelevant debug information. 285 | 286 | ```javascript 287 | o.spec("math", function() { 288 | // will not run 289 | o("addition", function() { 290 | o(1 + 1).equals(2) 291 | }) 292 | 293 | // this test will be run, regardless of how many groups there are 294 | o.only("subtraction", function() { 295 | o(1 - 1).notEquals(2) 296 | }) 297 | 298 | // will not run 299 | o("multiplication", function() { 300 | o(2 * 2).equals(4) 301 | }) 302 | 303 | // this test will be run, regardless of how many groups there are 304 | o.only("division", function() { 305 | o(6 / 2).notEquals(2) 306 | }) 307 | }) 308 | ``` 309 | 310 | ### Running the test suite 311 | 312 | ```javascript 313 | //define a test 314 | o("addition", function() { 315 | o(1 + 1).equals(2) 316 | }) 317 | 318 | //run the suite 319 | o.run() 320 | ``` 321 | 322 | ### Running test suites concurrently 323 | 324 | The `o.new()` method can be used to create new instances of ospec, which can be run in parallel. Note that each instance will report independently, and there's no aggregation of results. 325 | 326 | ```javascript 327 | var _o = o.new('optional name') 328 | _o("a test", function() { 329 | _o(1).equals(1) 330 | }) 331 | _o.run() 332 | ``` 333 | 334 | ## Command Line Interface 335 | 336 | Create a script in your package.json: 337 | 338 | ```javascript 339 | "scripts": { 340 | "test": "ospec", 341 | ... 342 | } 343 | ``` 344 | 345 | ...and run it from the command line: 346 | 347 | ```shell 348 | npm test 349 | ``` 350 | 351 | **NOTE:** `o.run()` is automatically called by the cli - no need to call it in your test code. 352 | 353 | ### CLI Options 354 | 355 | Running ospec without arguments is equivalent to running `ospec '**/tests/**/*.js'`. In english, this tells ospec to evaluate all `*.js` files in any sub-folder named `tests/` (the `node_modules` folder is always excluded). 356 | 357 | If you wish to change this behavior, just provide one or more glob match patterns: 358 | 359 | ```shell 360 | ospec '**/spec/**/*.js' '**/*.spec.js' 361 | ``` 362 | 363 | You can also provide ignore patterns (note: always add `--ignore` AFTER match patterns): 364 | 365 | ```shell 366 | ospec --ignore 'folder1/**' 'folder2/**' 367 | ``` 368 | 369 | Finally, you may choose to load files or modules before any tests run (**note:** always add `--preload` AFTER match patterns): 370 | 371 | ```shell 372 | ospec --preload esm 373 | ``` 374 | 375 | Here's an example of mixing them all together: 376 | 377 | ```shell 378 | ospec '**/*.test.js' --ignore 'folder1/**' --preload esm ./my-file.js 379 | ``` 380 | 381 | ### native mjs and module support 382 | 383 | For Node.js versions >= 13.2, `ospec` supports both ES6 modules and CommonJS packages out of the box. `--preload esm` is thus not needed in that case. 384 | 385 | ### Run ospec directly from the command line 386 | 387 | ospec comes with an executable named `ospec`. npm auto-installs local binaries to `./node_modules/.bin/`. You can run ospec by running `./node_modules/.bin/ospec` from your project root, but there are more convenient methods to do so that we will soon describe. 388 | 389 | ospec doesn't work when installed globally (`npm install -g`). Using global scripts is generally a bad idea since you can end up with different, incompatible versions of the same package installed locally and globally. 390 | 391 | Here are different ways of running ospec from the command line. This knowledge applies to not just ospec, but any locally installed npm binary. 392 | 393 | #### npx 394 | 395 | If you're using a recent version of npm (v5+), you can use run `npx ospec` from your project folder. 396 | 397 | #### npm-run 398 | 399 | If you're using a recent version of npm (v5+), you can use run `npx ospec` from your project folder. 400 | 401 | Otherwise, to work around this limitation, you can use [`npm-run`](https://www.npmjs.com/package/npm-run) which enables one to run the binaries of locally installed packages. 402 | 403 | ```shell 404 | npm install npm-run -g 405 | ``` 406 | 407 | Then, from a project that has ospec installed as a (dev) dependency: 408 | 409 | ```shell 410 | npm-run ospec 411 | ``` 412 | 413 | #### PATH 414 | 415 | If you understand how your system's PATH works (e.g. for [OSX](https://coolestguidesontheplanet.com/add-shell-path-osx/)), then you can add the following to your PATH... 416 | 417 | ```shell 418 | export PATH=./node_modules/.bin:$PATH 419 | ``` 420 | 421 | ...and you'll be able to run `ospec` without npx, npm, etc. This one-time setup will also work with other binaries across all your node projects, as long as you run binaries from the root of your projects. 422 | 423 | --- 424 | 425 | ## API 426 | 427 | Square brackets denote optional arguments 428 | 429 | ### void o.spec(String title, Function tests) 430 | 431 | Defines a group of tests. Groups are optional 432 | 433 | --- 434 | 435 | ### void o(String title, Function([Function done]) assertions) 436 | 437 | Defines a test. 438 | 439 | If an argument is defined for the `assertions` function, the test is deemed to be asynchronous, and the argument is required to be called exactly one time. 440 | 441 | --- 442 | 443 | ### Assertion o(any value) 444 | 445 | Starts an assertion. There are six types of assertion: `equals`, `notEquals`, `deepEquals`, `notDeepEquals`, `throws`, `notThrows`. 446 | 447 | Assertions have this form: 448 | 449 | ```shell 450 | o(actualValue).equals(expectedValue) 451 | ``` 452 | 453 | As a matter of convention, the actual value should be the first argument and the expected value should be the second argument in an assertion. 454 | 455 | Assertions can also accept an optional description curried parameter: 456 | 457 | ```javascript 458 | o(actualValue).equals(expectedValue)("this is a description for this assertion") 459 | ``` 460 | 461 | Assertion descriptions can be simplified using ES6 tagged template string syntax: 462 | 463 | ```javascript 464 | o(actualValue).equals(expectedValue) `this is a description for this assertion` 465 | ``` 466 | 467 | #### Function(String description) o(any value).equals(any value) 468 | 469 | Asserts that two values are strictly equal (`===`) 470 | 471 | #### Function(String description) o(any value).notEquals(any value) 472 | 473 | Asserts that two values are strictly not equal (`!==`) 474 | 475 | #### Function(String description) o(any value).deepEquals(any value) 476 | 477 | Asserts that two values are recursively equal 478 | 479 | #### Function(String description) o(any value).notDeepEquals(any value) 480 | 481 | Asserts that two values are not recursively equal 482 | 483 | #### Function(String description) o(Function fn).throws(Object constructor) 484 | 485 | Asserts that a function throws an instance of the provided constructor 486 | 487 | #### Function(String description) o(Function fn).throws(String message) 488 | 489 | Asserts that a function throws an Error with the provided message 490 | 491 | #### Function(String description) o(Function fn).notThrows(Object constructor) 492 | 493 | Asserts that a function does not throw an instance of the provided constructor 494 | 495 | #### Function(String description) o(Function fn).notThrows(String message) 496 | 497 | Asserts that a function does not throw an Error with the provided message 498 | 499 | --- 500 | 501 | ### void o.before(Function([Function done]) setup) 502 | 503 | Defines code to be run at the beginning of a test group 504 | 505 | If an argument is defined for the `setup` function, this hook is deemed to be asynchronous, and the argument is required to be called exactly one time. 506 | 507 | --- 508 | 509 | ### void o.after(Function([Function done) teardown) 510 | 511 | Defines code to be run at the end of a test group 512 | 513 | If an argument is defined for the `teardown` function, this hook is deemed to be asynchronous, and the argument is required to be called exactly one time. 514 | 515 | --- 516 | 517 | ### void o.beforeEach(Function([Function done]) setup) 518 | 519 | Defines code to be run before each test in a group 520 | 521 | If an argument is defined for the `setup` function, this hook is deemed to be asynchronous, and the argument is required to be called exactly one time. 522 | 523 | --- 524 | 525 | ### void o.afterEach(Function([Function done]) teardown) 526 | 527 | Defines code to be run after each test in a group 528 | 529 | If an argument is defined for the `teardown` function, this hook is deemed to be asynchronous, and the argument is required to be called exactly one time. 530 | 531 | --- 532 | 533 | ### void o.only(String title, Function([Function done]) assertions) 534 | 535 | Declares that only a single test should be run, instead of all of them 536 | 537 | --- 538 | 539 | ### Function o.spy([Function fn]) 540 | 541 | Returns a function that records the number of times it gets called, and each call's arguments 542 | 543 | #### Number o.spy().callCount 544 | 545 | The number of times the function has been called 546 | 547 | #### Array<any> o.spy().args 548 | 549 | The arguments that were passed to the function in the last time it was called 550 | 551 | #### Array<any> o.spy().calls 552 | 553 | An array representing all the times the function was called. Each array value is an object containing the `this` calling context and an `args` array. 554 | 555 | --- 556 | 557 | ### void o.run([Function reporter]) 558 | 559 | Runs the test suite. By default passing test results are printed using 560 | `console.log` and failing test results are printed using `console.error`. 561 | 562 | If you have custom continuous integration needs then you can use a 563 | reporter to process [test result data](#result-data) yourself. 564 | 565 | If running in Node.js, ospec will call `process.exit` after reporting 566 | results by default. If you specify a reporter, ospec will not do this 567 | and allow your reporter to respond to results in its own way. 568 | 569 | --- 570 | 571 | ### Number o.report(results) 572 | 573 | The default reporter used by `o.run()` when none are provided. Returns the number of failures, doesn't exit Node.js by itself. It expects an array of [test result data](#result-data) as argument. 574 | 575 | --- 576 | 577 | ### Function o.new() 578 | 579 | Returns a new instance of ospec. Useful if you want to run more than one test suite concurrently 580 | 581 | ```javascript 582 | var $o = o.new() 583 | $o("a test", function() { 584 | $o(1).equals(1) 585 | }) 586 | $o.run() 587 | ``` 588 | 589 | ### throwing Errors 590 | 591 | When an error is thrown some tests may be skipped. See the "run time model" for a detailed description of the bailout mechanism. 592 | 593 | --- 594 | 595 | ## Result data 596 | 597 | Test results are available by reference for integration purposes. You 598 | can use custom reporters in `o.run()` to process these results. 599 | 600 | ```javascript 601 | o.run(function(results) { 602 | // results is an array 603 | 604 | results.forEach(function(result) { 605 | // ... 606 | }) 607 | }) 608 | ``` 609 | 610 | --- 611 | 612 | ### Boolean|Null result.pass 613 | 614 | - `true` if the assertion passed. 615 | - `false` if the assertion failed. 616 | - `null` if the assertion was incomplete (`o("partial assertion) // and that's it`). 617 | 618 | --- 619 | 620 | ### Error result.error 621 | 622 | The `Error` object explaining the reason behind a failure. If the assertion failed, the stack will point to the actual error. If the assertion did pass or was incomplete, this field is identical to `result.testError`. 623 | 624 | --- 625 | 626 | ### Error result.testError 627 | 628 | An `Error` object whose stack points to the test definition that wraps the assertion. Useful as a fallback because in some async cases the main may not point to test code. 629 | 630 | --- 631 | 632 | ### String result.message 633 | 634 | If an exception was thrown inside the corresponding test, this will equal that Error's `message`. Otherwise, this will be a preformatted message in [SVO form](https://en.wikipedia.org/wiki/Subject%E2%80%93verb%E2%80%93object). More specifically, `${subject}\n${verb}\n${object}`. 635 | 636 | As an example, the following test's result message will be `"false\nshould equal\ntrue"`. 637 | 638 | ```javascript 639 | o.spec("message", function() { 640 | o(false).equals(true) 641 | }) 642 | ``` 643 | 644 | If you specify an assertion description, that description will appear two lines above the subject. 645 | 646 | ```javascript 647 | o.spec("message", function() { 648 | o(false).equals(true)("Candyland") // result.message === "Candyland\n\nfalse\nshould equal\ntrue" 649 | }) 650 | ``` 651 | 652 | --- 653 | 654 | ### String result.context 655 | 656 | A `>`-separated string showing the structure of the test specification. 657 | In the below example, `result.context` would be `testing > rocks`. 658 | 659 | ```javascript 660 | o.spec("testing", function() { 661 | o.spec("rocks", function() { 662 | o(false).equals(true) 663 | }) 664 | }) 665 | ``` 666 | 667 | --- 668 | 669 | ## Run time model 670 | 671 | ### Definitions 672 | 673 | - A **test** is the function passed to `o("description", function test() {})`. 674 | - A **hook** is a function passed to `o.before()`, `o.after()`. `o.beforeEach()` and `o.afterEach()`. 675 | - A **task** designates either a test or a hook. 676 | - A given test and its associated `beforeEach` and `afterEach` hooks form a **streak**. The `beforeEach` hooks run outermost first, the `afterEach` run outermost last. The hooks are optional, and are tied at test-definition time in the `o.spec()` calls that enclose the test. 677 | - A **spec** is a collection of streaks, specs, one `before` hook and one `after` hook. Each component is optional. Specs are defined with the `o.spec("spec name", function specDef() {})` calls. 678 | 679 | ### The phases of an ospec run 680 | 681 | For a given instance, an `ospec` run goes through three phases: 682 | 683 | 1) tests definition 684 | 1) tests execution and results accumulation 685 | 1) results presentation 686 | 687 | #### Tests definition 688 | 689 | This phase is synchronous. `o.spec("spec name", function specDef() {})`, `o("test name", function test() {})` and hooks calls generate a tree of specs and tests. 690 | 691 | #### Test execution and results accumulation 692 | 693 | At test execution time, for each spec, the `before` hook is called if present, then nested specs the streak of each test, in definition order, then the `after` hook, if present. 694 | 695 | Test and hooks may contain assertions, which will populate the `results` array. 696 | 697 | #### Results presentation 698 | 699 | Once all tests have run or timed out, the results are presented. 700 | 701 | ### Throwing errors and spec bail out 702 | 703 | While some testing libraries consider error thrown as assertions failure, `ospec` treats them as super-failures. Throwing will cause the current spec to be aborted, avoiding what can otherwise end up as pages of errors. What this means depends on when the error is thrown. Specifically: 704 | 705 | - A syntax error in a file causes the file to be ignored by the runner. 706 | - At test-definition time: 707 | - An error thrown at the root of a file will cause subsequent tests and specs to be ignored. 708 | - An error thrown in a spec definition will cause the spec to be ignored. 709 | - At test-execution time: 710 | - An error thrown in the `before` hook will cause the streaks and nested specs to be ignored. The `after` hook will run. 711 | - An error thrown in a task... 712 | - ...prevents further streaks and nested specs in the current spec from running. The `after` *hook* of the spec will run. 713 | - ...if thrown in a `beforeEach` hook of a streak, causes the streak to be hollowed out. Hooks defined in nested scopes and the actual test will not run. However, the `afterEach` hook corresponding to the one that crashed will run, as will those defined in outer scopes. 714 | 715 | For every error thrown, a "bail out" failure is reported. 716 | 717 | --- 718 | 719 | ## Goals 720 | 721 | Ospec started as a bare bones test runner optimized for Leo Horie to write Mithril v1 with as little hassle as possible. It has since grown in capabilities and polish, and while we tried to keep some of the original spirit, the current incarnation is not as radically minimalist as the original. The state of the art in testing has also moved with the dominance of Jest over Jasmine and Mocha, and now Vitest coming up the horizon. 722 | 723 | - Do the most common things that the mocha/chai/sinon triad does without having to install 3 different libraries and several dozen dependencies 724 | - Limit configuration in test-space: 725 | - Disallow ability to pick between API styles (BDD/TDD/Qunit, assert/should/expect, etc) 726 | - No "magic" plugin system with global reach. Custom assertions need to be imported or defined lexically (e.g. in `o(value)._(matches(refence))`, `matches` can be resolved in file). 727 | - Provide a default simple reporter 728 | - Make assertion code terse, readable and self-descriptive 729 | - Have as few assertion types as possible for a workable usage pattern 730 | 731 | These restrictions have a few benefits: 732 | 733 | - tests always look the same, even across different projects and teams 734 | - single source of documentation for entire testing API 735 | - no need to hunt down plugins to figure out what they do, especially if they replace common javascript idioms with fuzzy spoken language constructs (e.g. what does `.is()` do?) 736 | - no need to pollute project-space with ad-hoc configuration code 737 | - discourages side-tracking and yak-shaving 738 | -------------------------------------------------------------------------------- /bin/ospec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict" 3 | 4 | const o = require("../ospec") 5 | const path = require("path") 6 | const glob = require("glob") 7 | 8 | const loaderDetected = new Promise((fulfill, reject) => { 9 | try { 10 | // eval is needed because`import()` is a syntax error in older node.js versions (pre 13.2) 11 | // also some node versions (12.13+ at least) do support the syntax but reject the promise 12 | // at run time... 13 | // eslint-disable-next-line no-eval 14 | eval(` 15 | import('file:./non-existent-file').catch((e)=>{ 16 | if (e.message.includes('Not supported')) reject() 17 | else fulfill() 18 | })`) 19 | } catch(_) { 20 | reject() 21 | } 22 | }).then( 23 | // eslint-disable-next-line no-eval, no-unused-vars 24 | () => (x) => eval("import('file:' + x)"), 25 | // eslint-disable-next-line global-require 26 | () => async (x) => require(x) 27 | ) 28 | 29 | function parseArgs(argv) { 30 | argv = ["--globs"].concat(argv.slice(2)) 31 | const args = {preload: []} 32 | let name 33 | argv.forEach((arg) => { 34 | if ((/^--/).test(arg)) { 35 | name = arg.slice(2) 36 | if (name === "require") { 37 | if (args.require == null) console.warn( 38 | "Warning: The --require option has been deprecated, use --preload instead" 39 | ) 40 | args.require = true 41 | name = "preload" 42 | } 43 | args[name] = args[name] || [] 44 | } else { 45 | args[name].push(arg) 46 | } 47 | }) 48 | return args 49 | } 50 | 51 | 52 | const args = parseArgs(process.argv) 53 | const globList = args.globs && args.globs.length ? args.globs : ["**/tests/**/*.js"] 54 | const ignore = ["**/node_modules/**"].concat(args.ignore || []) 55 | const cwd = process.cwd() 56 | 57 | loaderDetected.then((load) => { 58 | Promise.all(args.preload.map( 59 | (mod) => load(path.join(cwd, mod)).catch((e) => { 60 | console.error(`could not preload ${mod}`) 61 | console.error(e) 62 | // eslint-disable-next-line no-process-exit 63 | process.exit(1) 64 | }) 65 | )).then( 66 | () => { 67 | let remaining = globList.length 68 | let loading = Promise.resolve() 69 | globList.forEach((globPattern) => { 70 | glob.globStream(globPattern, {ignore: ignore}) 71 | .on("data", (fileName) => { 72 | var fullPath = path.join(cwd, fileName) 73 | loading = loading.then(() => { 74 | o.metadata({file: fullPath}) 75 | return load(fullPath).catch((e) => { 76 | console.error(e) 77 | o.spec(path.join(cwd, fileName), () => { 78 | o("> > BAILED OUT < < <", function(){throw e}) 79 | }) 80 | }) 81 | }) 82 | }) 83 | .on("error", (e) => { console.error(e) }) 84 | .on("end", () => { if (--remaining === 0) loading.then(() => o.run()) }) 85 | }) 86 | } 87 | ) 88 | }) 89 | 90 | process.on("unhandledRejection", (e) => { console.error("Uncaught (in promise) " + e.stack) }) 91 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Change log for ospec 2 | 3 | - [Upcoming](#upcoming) 4 | - [4.2.0](#420) 5 | - [4.1.7](#417) 6 | - [4.1.6](#416) 7 | - [4.1.5](#415) 8 | - [4.1.4](#414) 9 | - [4.1.3](#413) 10 | - [4.1.2](#412) 11 | - [4.1.1](#411) 12 | - [4.1.0](#410) 13 | - [4.0.1](#401) 14 | - [4.0.0](#400) 15 | - [3.1.0](#310) 16 | - [3.0.1](#301) 17 | - [3.0.0](#300) 18 | - [2.1.0](#210) 19 | - [2.0.0](#200) 20 | - [1.4.1](#141) 21 | - [1.4.0](#140) 22 | - [1.3 and earlier](#13-and-earlier) 23 | 24 | 25 | Change log 26 | ====== 27 | 28 | ### Upcoming 29 | 30 | 31 | ### 4.2.1 32 | _2024-09-02_ 33 | 34 | - Update `glob` dependency to v9. 35 | 36 | ### 4.2.0 37 | _2023-03-10_ 38 | 39 | - new API that timetout and race-condition proof. Assertions and spies that run after a test is done are registered as failures. This is opt-in for now, it will become the default in v5 (the old API will become opt-in at that point to ease the transition). 40 | 41 | #### Bug fix 42 | 43 | - Escape percent signs in the report in Node.js, fix [#57](https://github.com/MithrilJS/ospec/issue/57). Thanks to [@LeXofLeviafan](https://github.com/LeXofLeviafan) for the report. 44 | 45 | ### 4.1.7 46 | _2023-01-20_ 47 | 48 | #### Bug fixes 49 | 50 | - Explicitly `exit()` the process after all test have passed, to avoid the process hanging due to a dangling `setInterval` somewhere (by [Már Örlygsson](https://github.com/maranomynet), [#51](https://github.com/MithrilJS/ospec/pull/51)). 51 | - Tweak the CLI test suite to accomodate the new `pnpm` output on error. 52 | - Misc repo maintenance 53 | 54 | ### 4.1.6 55 | _2022-05-19_ 56 | 57 | - `.deepEquals()` now ignores non-enumerable keys. This makes `seamless-immutable` objects comparable and fixes [#24](https://github.com/MithrilJS/ospec/issues/24). 58 | 59 | ### 4.1.5 60 | _2022-05-19_ 61 | 62 | #### Bug fix 63 | 64 | - Properly interpolate values when using assertions to tag templages 65 | 66 | ```JS 67 | o(x).equals(y)`Description that interpolates ${context}` 68 | ``` 69 | 70 | Will work as expected. This fixes [#43](https://github.com/MithrilJS/ospec/issues/43) 71 | 72 | ### 4.1.4 73 | _2022-05-19_ 74 | 75 | #### Bug fixes 76 | 77 | - Work around a Rollup limitation, fixes [#25](https://github.com/MithrilJS/ospec/issues/25) Thanks to [Ivan Kupalov](https://github.com/charlag) for the report and preliminary fix. 78 | - Properly handle objects with a null prototype in `.deepEquals` assertions. Fixes [#41](https://github.com/MithrilJS/ospec/issues/41) 79 | 80 | ### 4.1.3 81 | 82 | #### Bug fix 83 | _2022-05-18_ 84 | - Fix post-install crash introduced in v4.1.2 85 | 86 | ### 4.1.2 87 | 88 | #### Bug fixes 89 | _2022-05-17_ 90 | - Properly handle ES6 modules in Widows ([#30](https://github.com/MithrilJS/ospec/pull/30), closes (#35)[https://github.com/MithrilJS/ospec/issues/35] 91 | 92 | ### 4.1.1 93 | _2020-04-07_ 94 | 95 | #### Bug fixes 96 | - Fix the runner for Node.js v12 (which parses dynamic `import()` calls, but rejects the promise) 97 | - Fix various problems with the tests 98 | 99 | ### 4.1.0 100 | _2020-04-06_ 101 | 102 | - General cleanup and source comments. Drop the "300 LOC" pretense. `ospec` has grown quite a bit, possibly to the point where it needs a new name, since the lib has diverged quite a bit from its original philosophy ([#18](https://github.com/MithrilJS/ospec/pull/18)) 103 | - Add native support for ES modules in Node versions that support it ([#13](https://github.com/MithrilJS/ospec/pull/13)) 104 | - deprecate `--require` and intrduce `--preload` since it can not load both CommonJS packages or ES6 modules (`--require` is still supported with a warning for easing the transition). 105 | - Add a test suite for the CLI runner ((cjs, esm) × (npm, yarn, nodejs)) ([#17](https://github.com/MithrilJS/ospec/pull/17)) 106 | - Improve ergonomics when tests fail. ([#18](https://github.com/MithrilJS/ospec/pull/18)) 107 | - Correctly label assertions that happen in hooks. 108 | - Errors thrown cause the current spec to be interrupted ("bail out") 109 | - The test runner tolerates load-time failures and reports them. 110 | - Once a test has timed out, assertions may be mislabeled. They are now labelled with `???` until the timed out test finishes. 111 | - Add experimental `.satisfies` and `.notSatisfies` assertions ([#18](https://github.com/MithrilJS/ospec/pull/18), partially address [#12](https://github.com/MithrilJS/ospec/issues/12)). 112 | - Add `o.metadata()` which, with `o().statisfies()` opens the door to snapshots. ([#18](https://github.com/MithrilJS/ospec/pull/18)) 113 | 114 | #### Bug fixes 115 | 116 | - The `timeout` argument for tests has long been declared deprecated, but they were still documented and didn't issue any warning on use. Not anymore ([#18](https://github.com/MithrilJS/ospec/pull/18)) 117 | - Give spies the name and length of the functions they wrap in ES5 environments ([#18](https://github.com/MithrilJS/ospec/pull/18), fixes [#8](https://github.com/MithrilJS/ospec/issues/8)) 118 | - Make the `o.only` warning tighter and harder to ignore ([#18](https://github.com/MithrilJS/ospec/pull/18)) 119 | - Lock Zalgo back in (the first test was being run synchronously unlike the following ones, except in browsers, where the 5000 first tests could have run before the first `setTimout()` call) ([#18](https://github.com/MithrilJS/ospec/pull/18)) 120 | - Fix another corner case with the done parser [#16](https://github.com/MithrilJS/ospec/pull/16) [@kfule](https://github.com/kfule) 121 | - Fix arrow functions (`(done) => { }`) support in asynchronous tests. ([#2](https://github.com/MithrilJS/ospec/pull/2) [@kesara](https://github.com/kesara)) 122 | 123 | ### 4.0.1 124 | _2019-08-18_ 125 | 126 | - Fix `require` with relative paths 127 | 128 | ### 4.0.0 129 | _2019-07-24_ 130 | 131 | - Pull ESM support out 132 | 133 | ### 3.1.0 134 | _2019-02-07_ 135 | 136 | - ospec: Test results now include `.message` and `.context` regardless of whether the test passed or failed. (#2227 @robertakarobin) 137 | - Add `spy.calls` array property to get the `this` and `arguments` values for any arbitrary call. (#2221 @isiahmeadows) 138 | - Added `.throws` and `.notThrows` assertions to ospec. (#2255 @robertakarobin) 139 | - Update `glob` dependency. 140 | 141 | ### 3.0.1 142 | _2018-06-30_ 143 | 144 | #### Bug fix 145 | - Move `glob` from `devDependencies` to `dependencies`, fix the test runner ([#2186](https://github.com/MithrilJS/mithril.js/pull/2186) [@porsager](https://github.com/porsager) 146 | 147 | ### 3.0.0 148 | _2018-06-26_ 149 | 150 | #### Breaking 151 | - Better input checking to prevent misuses of the library. Misues of the library will now throw errors, rather than report failures. This may uncover bugs in your test suites. Since it is potentially a disruptive update this change triggers a semver major bump. ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167)) 152 | - Change the reserved character for hooks and test suite meta-information from `"__"` to `"\x01"`. Tests whose name start with `"\0x01"` will be rejected ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167)) 153 | 154 | #### Features 155 | - Give async timeout a stack trace that points to the problematic test ([#2154](https://github.com/MithrilJS/mithril.js/pull/2154) [@gilbert](github.com/gilbert), [#2167](https://github.com/MithrilJS/mithril.js/pull/2167)) 156 | - deprecate the `timeout` parameter in async tests in favour of `o.timeout()` for setting the timeout delay. The `timeout` parameter still works for v3, and will be removed in v4 ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167)) 157 | - add `o.defaultTimeout()` for setting the the timeout delay for the current spec and its children ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167)) 158 | - adds the possibility to select more than one test with o.only ([#2171](https://github.com/MithrilJS/mithril.js/pull/2171)) 159 | 160 | #### Bug fixes 161 | - Detect duplicate calls to `done()` properly [#2162](https://github.com/MithrilJS/mithril.js/issues/2162) ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167)) 162 | - Don't try to report internal errors as assertion failures, throw them instead ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167)) 163 | - Don't ignore, silently, tests whose name start with the test suite meta-information sequence (was `"__"` up to this version) ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167)) 164 | - Fix the `done()` call detection logic [#2158](https://github.com/MithrilJS/mithril.js/issues/2158) and assorted fixes (accept non-English names, tolerate comments) ([#2167](https://github.com/MithrilJS/mithril.js/pull/2167)) 165 | - Catch exceptions thrown in synchronous tests and report them as assertion failures ([#2171](https://github.com/MithrilJS/mithril.js/pull/2171)) 166 | - Fix a stack overflow when using `o.only()` with a large test suite ([#2171](https://github.com/MithrilJS/mithril.js/pull/2171)) 167 | 168 | ### 2.1.0 169 | _2018-05-25_ 170 | 171 | #### Features 172 | - Pinpoint the `o.only()` call site ([#2157](https://github.com/MithrilJS/mithril.js/pull/2157)) 173 | - Improved wording, spacing and color-coding of report messages and errors ([#2147](https://github.com/MithrilJS/mithril.js/pull/2147), [@maranomynet](https://github.com/maranomynet)) 174 | 175 | #### Bug fixes 176 | - Convert the exectuable back to plain ES5 [#2160](https://github.com/MithrilJS/mithril.js/issues/2160) ([#2161](https://github.com/MithrilJS/mithril.js/pull/2161)) 177 | 178 | 179 | ### 2.0.0 180 | _2018-05-09_ 181 | 182 | - Added `--require` feature to the ospec executable ([#2144](https://github.com/MithrilJS/mithril.js/pull/2144), [@gilbert](https://github.com/gilbert)) 183 | - In Node.js, ospec only uses colors when the output is sent to a terminal ([#2143](https://github.com/MithrilJS/mithril.js/pull/2143)) 184 | - the CLI runner now accepts globs as arguments ([#2141](https://github.com/MithrilJS/mithril.js/pull/2141), [@maranomynet](https://github.com/maranomynet)) 185 | - Added support for custom reporters ([#2020](https://github.com/MithrilJS/mithril.js/pull/2020), [@zyrolasting](https://github.com/zyrolasting)) 186 | - Make ospec more [Flems](https://flems.io)-friendly ([#2034](https://github.com/MithrilJS/mithril.js/pull/2034)) 187 | - Works either as a global or in CommonJS environments 188 | - the o.run() report is always printed asynchronously (it could be synchronous before if none of the tests were async). 189 | - Properly point to the assertion location of async errors [#2036](https://github.com/MithrilJS/mithril.js/issues/2036) 190 | - expose the default reporter as `o.report(results)` 191 | - Don't try to access the stack traces in IE9 192 | 193 | 194 | 195 | ### 1.4.1 196 | _2018-05-03_ 197 | 198 | - Identical to v1.4.0, but with UNIX-style line endings so that BASH is happy. 199 | 200 | 201 | 202 | ### 1.4.0 203 | _2017-12-01_ 204 | 205 | - Added support for async functions and promises in tests ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928), [@StephanHoyer](https://github.com/StephanHoyer)) 206 | - Error handling for async tests with `done` callbacks supports error as first argument ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928)) 207 | - Error messages which include newline characters do not swallow the stack trace [#1495](https://github.com/MithrilJS/mithril.js/issues/1495) ([#1984](https://github.com/MithrilJS/mithril.js/pull/1984), [@RodericDay](https://github.com/RodericDay)) 208 | 209 | 210 | 211 | ### 1.3 and earlier 212 | 213 | - Log using util.inspect to show object content instead of "[object Object]" ([#1661](https://github.com/MithrilJS/mithril.js/issues/1661), [@porsager](https://github.com/porsager)) 214 | - Shell command: Ignore hidden directories and files ([#1855](https://github.com/MithrilJS/mithril.js/pull/1855) [@pdfernhout)](https://github.com/pdfernhout)) 215 | - Library: Add the possibility to name new test suites ([#1529](https://github.com/MithrilJS/mithril.js/pull/1529)) 216 | -------------------------------------------------------------------------------- /ospec.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | // const LOG = console.log 3 | // const p = (...args) => { 4 | // LOG(...args) 5 | // return args.pop() 6 | // } 7 | 8 | /* 9 | Ospec is made of four parts: 10 | 11 | 1. a test definition API That creates a spec/tests tree 12 | 2. a test runner that walks said spec tree 13 | 3. an assertion API that populates a results array 14 | 4. a reporter which presents the results 15 | 16 | The tepmoral sequence at run time is 1 then (2 and 3), then 4 17 | 18 | The various sections (and sub-sections thereof) share information through stack-managed globals 19 | which are enumerated in the "Setup" section below. 20 | 21 | there are three kind of data structures, that reflect the above segregation: 22 | 23 | 1. Specs, that group other specs and tasks 24 | 2. Tasks, that represent hooks and tests, and internal logic 25 | 3. Assertions which end up in the results array. 26 | 27 | At run-time, the specs are converted to lists of task (one per entry in the spec) 28 | In each of these tasks: 29 | - sub-specs receive the same treament as their parent, when their turn comes. 30 | - tests are also turned into lists of tasks [...beforeEach, test, ...afterEach] 31 | */ 32 | 33 | 34 | ;(function(m) { 35 | if (typeof module !== "undefined") module["exports"] = m() 36 | else window.o = m() 37 | })(function init(name) { 38 | // # Setup 39 | // const 40 | var hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty 41 | 42 | var hasSuiteName = arguments.length !== 0 43 | var only = [] 44 | var ospecFileName = getStackName(ensureStackTrace(new Error), /[\/\\](.*?):\d+:\d+/) 45 | var rootSpec = new Spec() 46 | var subjects = [] 47 | 48 | // stack-managed globals 49 | 50 | // Are we in the process of baling out? 51 | var $bail = false 52 | // current spec 53 | var $context = rootSpec 54 | // spec nesting level 55 | var $depth = 1 56 | // the current file name 57 | var $file 58 | // the task (test/hook) that is currently running 59 | var $task = null 60 | // the current o.timeout implementation 61 | var $timeout 62 | // count the total amount of tests that timed out and didn't complete after the fact before the run ends 63 | var $timedOutAndPendingResolution = 0 64 | // are we using the v5+ API? 65 | var $localAssertions = false 66 | // Shared state, set only once, but initialization is delayed 67 | var results, stats, timeoutStackName 68 | 69 | // # General utils 70 | function isRunning() {return results != null} 71 | 72 | function ensureStackTrace(error) { 73 | // mandatory to get a stack in IE 10 and 11 (and maybe other envs?) 74 | if (error.stack === undefined) try { throw error } catch(e) {return e} 75 | else return error 76 | } 77 | 78 | function getStackName(e, exp) { 79 | return e.stack && exp.test(e.stack) ? e.stack.match(exp)[1] : null 80 | } 81 | 82 | 83 | function timeoutParamDeprecationNotice(n) { 84 | console.error(new Error("`timeout()` as a test argument has been deprecated, use `o.timeout()`")) 85 | o.timeout(n) 86 | } 87 | 88 | // TODO: handle async functions? 89 | function validateDone(fn, error) { 90 | if (error == null || fn.length === 0) return 91 | var body = fn.toString() 92 | // Don't change the RegExp by hand, it is generated by 93 | // `scripts/build-done-parser.js`. 94 | // If needed, update the script and paste its output here. 95 | var arg = (body.match(/^(?:(?:function(?:\s|\/\*[^]*?\*\/|\/\/[^\n]*\n)*(?:\b[^\s(\/]+(?:\s|\/\*[^]*?\*\/|\/\/[^\n]*\n)*)?)?\((?:\s|\/\*[^]*?\*\/|\/\/[^\n]*\n)*)?([^\s{[),=\/]+)/) || []).pop() 96 | if (arg) { 97 | if(body.indexOf(arg) === body.lastIndexOf(arg)) { 98 | var doneError = new Error 99 | doneError.stack = "'" + arg + "()' should be called at least once\n" + o.cleanStackTrace(error) 100 | throw doneError 101 | } 102 | } else { 103 | console.warn("we couldn't determine the `done` callback name, please file a bug report at https://github.com/mithriljs/ospec/issues") 104 | arg = "done" 105 | } 106 | return "`" + arg + "()` should only be called once" 107 | } 108 | 109 | // # Spec definition 110 | function Spec() { 111 | this.before = [] 112 | this.beforeEach = [] 113 | this.after = [] 114 | this.afterEach = [] 115 | this.specTimeout = null 116 | this.customAssert = null 117 | this.children = Object.create(null) 118 | } 119 | 120 | // Used for both user-defined tests and internal book keeping 121 | // Internal tasks don't have an `err`. `hookName` is only defined 122 | // for hooks 123 | function Task(fn, err, hookName) { 124 | // This test needs to be here rather than in `o("name", test(){})` 125 | // in order to also cover nested hooks. 126 | if (isRunning() && err != null) throw new Error("Test definitions and hooks shouldn't be nested. To group tests, use 'o.spec()'") 127 | this.context = null 128 | this.file = $file 129 | // give tests an extra level of depth (simplifies bail out logic) 130 | this.depth = $depth + (hookName == null ? 1 : 0) 131 | this.doneTwiceError = !$localAssertions && validateDone(fn, err) || "A thenable should only be resolved once" 132 | this.error = err 133 | this.internal = err == null 134 | this.fn = fn 135 | this.hookName = hookName 136 | this.localAssertions = $localAssertions 137 | } 138 | 139 | function hook(name) { 140 | return function(predicate) { 141 | if ($context[name].length > 0) throw new Error("Attempt to register o." + name + "() more than once. A spec can only have one hook of each kind") 142 | $context[name][0] = new Task(predicate, ensureStackTrace(new Error), name) 143 | } 144 | } 145 | 146 | function unique(subject) { 147 | if (hasOwn.call($context.children, subject)) { 148 | console.warn("A test or a spec named '" + subject + "' was already defined in this spec") 149 | console.warn(o.cleanStackTrace(ensureStackTrace(new Error)).split("\n")[0]) 150 | while (hasOwn.call($context.children, subject)) subject += "*" 151 | } 152 | return subject 153 | } 154 | 155 | // # API 156 | function o(subject, predicate) { 157 | if (predicate === undefined) { 158 | if (!isRunning()) throw new Error("Assertions should not occur outside test definitions") 159 | if ($task.localAssertions) throw new SyntaxError("Illegal global assertion, use a local `o()`") 160 | return new Assertion(subject) 161 | } else { 162 | subject = String(subject) 163 | $context.children[unique(subject)] = new Task(predicate, ensureStackTrace(new Error), null) 164 | } 165 | } 166 | function noSpecOrTestHasBeenDefined() { 167 | return $context === rootSpec 168 | && Object.keys(rootSpec).every(k => { 169 | const item = rootSpec[k] 170 | return item == null || (Array.isArray(item) ? item : Object.keys(item)).length === 0 171 | }) 172 | } 173 | o.globalAssertions = function(cb) { 174 | if (isRunning()) throw new SyntaxError("local/global modes can only be called before o.run()") 175 | if (cb === "override"){ 176 | // escape hatch for the CLI test suite 177 | // ideally we should use --preload that requires 178 | // in depth rethinking of the CLI test suite that I'd rather 179 | // avoid while changing the core at the same time. 180 | $localAssertions = false 181 | return 182 | } else if (cb == null) { 183 | if (noSpecOrTestHasBeenDefined()) { 184 | $localAssertions = false 185 | return 186 | } else { 187 | throw new SyntaxError("local/global mode can only be toggled before defining specs and tests") 188 | } 189 | } else { 190 | const previous = $localAssertions 191 | try { 192 | $localAssertions = false 193 | cb() 194 | } finally { 195 | $localAssertions = previous 196 | } 197 | } 198 | } 199 | o.localAssertions = function(cb) { 200 | if (isRunning()) throw new SyntaxError("local/global modes can only be called before o.run()") 201 | if (cb === "override"){ 202 | // escape hatch for the CLI test suite 203 | // ideally we should use --preload that requires 204 | // in depth rethinking of the CLI test suite that I'd rather 205 | // avoid while changing the core at the same time. 206 | $localAssertions = true 207 | return 208 | } if (cb == null) { 209 | if (noSpecOrTestHasBeenDefined()) { 210 | $localAssertions = true 211 | } else { 212 | throw new SyntaxError("local/global mode can only be toggled before defining specs and tests") 213 | } 214 | } else { 215 | const previous = $localAssertions 216 | try { 217 | $localAssertions = true 218 | cb() 219 | } finally { 220 | $localAssertions = previous 221 | } 222 | } 223 | } 224 | 225 | o.before = hook("before") 226 | o.after = hook("after") 227 | o.beforeEach = hook("beforeEach") 228 | o.afterEach = hook("afterEach") 229 | 230 | o.specTimeout = function (t) { 231 | if (isRunning()) throw new Error("o.specTimeout() can only be called before o.run()") 232 | if ($context.specTimeout != null) throw new Error("A default timeout has already been defined in this context") 233 | if (typeof t !== "number") throw new Error("o.specTimeout() expects a number as argument") 234 | $context.specTimeout = t 235 | } 236 | 237 | o.new = init 238 | 239 | o.spec = function(subject, predicate) { 240 | if (isRunning()) throw new Error("`o.spec()` can't only be called at test definition time, not run time") 241 | // stack managed globals 242 | var parent = $context 243 | var name = unique(subject) 244 | $context = $context.children[name] = new Spec() 245 | $depth++ 246 | try { 247 | predicate() 248 | } finally { 249 | $depth-- 250 | $context = parent 251 | } 252 | } 253 | 254 | var onlyCalledAt = [] 255 | o.only = function(subject, predicate) { 256 | onlyCalledAt.push(o.cleanStackTrace(ensureStackTrace(new Error)).split("\n")[0]) 257 | only.push(predicate) 258 | o(subject, predicate) 259 | } 260 | 261 | o.cleanStackTrace = function(error) { 262 | // For IE 10+ in quirks mode, and IE 9- in any mode, errors don't have a stack 263 | if (error.stack == null) return "" 264 | var header = error.message ? error.name + ": " + error.message : error.name, stack 265 | // some environments add the name and message to the stack trace 266 | if (error.stack.indexOf(header) === 0) { 267 | stack = error.stack.slice(header.length).split(/\r?\n/) 268 | stack.shift() // drop the initial empty string 269 | } else { 270 | stack = error.stack.split(/\r?\n/) 271 | } 272 | if (ospecFileName == null) return stack.join("\n") 273 | // skip ospec-related entries on the stack 274 | return stack.filter(function(line) { return line.indexOf(ospecFileName) === -1 }).join("\n") 275 | } 276 | 277 | o.timeout = function(n) { 278 | $timeout(n) 279 | } 280 | 281 | // # Test runner 282 | var stack = [] 283 | var scheduled = false 284 | function cycleStack() { 285 | try { 286 | while (stack.length) stack.shift()() 287 | } finally { 288 | // Don't stop on error, but still let it propagate to the host as usual. 289 | if (stack.length) setTimeout(cycleStack, 0) 290 | else scheduled = false 291 | } 292 | } 293 | /* eslint-disable indent */ 294 | var nextTickish = hasProcess 295 | ? process.nextTick 296 | : typeof Promise === "function" 297 | ? Promise.prototype.then.bind(Promise.resolve()) 298 | : function fakeFastNextTick(next) { 299 | if (!scheduled) { 300 | scheduled = true 301 | setTimeout(cycleStack, 0) 302 | } 303 | stack.push(next) 304 | } 305 | /* eslint-enable indent */ 306 | o.metadata = function(opts) { 307 | if (arguments.length === 0) { 308 | if (!isRunning()) throw new Error("getting `o.metadata()` is only allowed at test run time") 309 | return { 310 | file: $task.file, 311 | name: $task.context 312 | } 313 | } else { 314 | if (isRunning() || $context !== rootSpec) throw new Error("setting `o.metadata()` is only allowed at the root, at test definition time") 315 | $file = opts.file 316 | } 317 | } 318 | 319 | o.run = function(reporter) { 320 | if (rootSpec !== $context) throw new Error("`o.run()` can't be called from within a spec") 321 | if (isRunning()) throw new Error("`o.run()` has already been called") 322 | results = [] 323 | stats = { 324 | bailCount: 0, 325 | onlyCalledAt: onlyCalledAt 326 | } 327 | 328 | if (hasSuiteName) { 329 | var parent = new Spec() 330 | parent.children[name] = rootSpec 331 | } 332 | 333 | var finalize = new Task(function() { 334 | timeoutStackName = getStackName({stack: o.cleanStackTrace(ensureStackTrace(new Error))}, /([w .]+?:d+:d+)/) 335 | if (typeof reporter === "function") reporter(results, stats) 336 | else { 337 | var errCount = o.report(results, stats) 338 | if (hasProcess) process.exit(errCount !== 0 ? 1 : undefined) // eslint-disable-line no-process-exit 339 | } 340 | }, null, null) 341 | 342 | // always async for consistent external behavior 343 | // otherwise, an async test would release Zalgo 344 | // https://blog.izs.me/2013/08/designing-apis-for-asynchrony 345 | nextTickish(function () { 346 | runSpec(hasSuiteName ? parent : rootSpec, [], [], finalize, 200 /*default timeout delay*/) 347 | }) 348 | 349 | function runSpec(spec, beforeEach, afterEach, finalize, defaultDelay) { 350 | var bailed = false 351 | if (spec.specTimeout) defaultDelay = spec.specTimeout 352 | 353 | // stack-managed globals 354 | var previousBail = $bail 355 | $bail = function() {bailed = true; stats.bailCount++} 356 | var restoreStack = new Task(function() { 357 | $bail = previousBail 358 | }, null, null) 359 | 360 | beforeEach = [].concat( 361 | beforeEach, 362 | spec.beforeEach 363 | ) 364 | afterEach = [].concat( 365 | spec.afterEach, 366 | afterEach 367 | ) 368 | series( 369 | [].concat( 370 | spec.before, 371 | Object.keys(spec.children).reduce(function(tasks, key) { 372 | if ( 373 | // If in `only` mode, skip the tasks that are not flagged to run. 374 | only.length === 0 375 | || only.indexOf(spec.children[key].fn) !== -1 376 | // Always run specs though, in case there are `only` tests nested in there. 377 | || !(spec.children[key] instanceof Task) 378 | ) { 379 | tasks.push(new Task(function(done) { 380 | if (bailed) return done() 381 | subjects.push(key) 382 | var popSubjects = new Task(function pop() {subjects.pop(); done()}, null, null) 383 | if (spec.children[key] instanceof Task) { 384 | // this is a test 385 | series( 386 | [].concat(beforeEach, spec.children[key], afterEach, popSubjects), 387 | defaultDelay 388 | ) 389 | } else { 390 | // a spec... 391 | runSpec(spec.children[key], beforeEach, afterEach, popSubjects, defaultDelay) 392 | } 393 | }, null, null)) 394 | } 395 | return tasks 396 | }, []), 397 | spec.after, 398 | restoreStack, 399 | finalize 400 | ), 401 | defaultDelay 402 | ) 403 | } 404 | 405 | // Executes a list of tasks in series. 406 | // This is quite convoluted because we handle both sync and async tasks. 407 | // Async tasks can either use a legacy `done(error?)` API, or return a 408 | // thenable, which may or may not behave like a Promise 409 | function series(tasks, defaultDelay) { 410 | var cursor = 0 411 | next() 412 | 413 | function next() { 414 | if (cursor === tasks.length) return 415 | 416 | // const 417 | var task = tasks[cursor++] 418 | var fn = task.fn 419 | var isHook = task.hookName != null 420 | var taskStartTime = new Date 421 | 422 | // let 423 | var delay = defaultDelay 424 | var hasMovedOn = false 425 | var hasConcluded = false 426 | var timeout 427 | 428 | var isDone = false 429 | var isAsync = false 430 | var promises = [] 431 | 432 | if (task.internal) { 433 | // internal tasks still use the legacy done() system. 434 | // handled hereafter in a simplified fashion, without timeout 435 | // and bailout handling (let it crash) 436 | if (fn.length === 0) { 437 | fn() 438 | next() 439 | } 440 | else fn(function() { 441 | if (hasMovedOn) throw new Error("Internal Error, done() should only be called once") 442 | hasMovedOn = true 443 | next() 444 | }) 445 | return 446 | } 447 | 448 | $task = task 449 | task.context = subjects.join(" > ") 450 | if (isHook) { 451 | task.context = "o." + task.hookName + Array.apply(null, {length: task.depth}).join("*") + "( " + task.context + " )" 452 | } 453 | 454 | $timeout = function timeout (t) { 455 | if (isAsync || hasConcluded || isDone) throw new Error("`o.timeout()` must be called synchronously from within a test definition or a hook") 456 | if (typeof t !== "number") throw new Error("timeout() and o.timeout() expect a number as argument") 457 | delay = t 458 | } 459 | if (task.localAssertions) { 460 | var assert = function o(value) { 461 | 462 | return new Assertion(value, task) 463 | } 464 | assert.metadata = o.metadata 465 | assert.timeout = o.timeout 466 | assert.spy = createSpy(function(self, args, fn, spy) { 467 | if (hasConcluded) fail(new Assertion().i, "spy ran after its test was concluded\n"+fn.toString()) 468 | return globalSpyHelper(self, args, fn, spy) 469 | }) 470 | assert.o = assert 471 | 472 | Object.defineProperty(assert, "done", {get(){ 473 | let f, r 474 | promises.push(new Promise((_f, _r)=>{f = _f, r = _r})) 475 | function done(x){ 476 | return x == null ? f() : r(x) 477 | } 478 | return done 479 | }}) 480 | 481 | // runs when a test had an error or returned a promise 482 | const conclude = (err, threw) => { 483 | if (threw) { 484 | if (err instanceof Error) fail(new Assertion().i, err.message, err) 485 | else fail(new Assertion().i, String(err), null) 486 | $bail() 487 | if (task.hookName === "beforeEach") { 488 | while (!task.internal && tasks[cursor].depth > task.depth) cursor++ 489 | } 490 | } 491 | if (timeout !== undefined) { 492 | timeout = clearTimeout(timeout) 493 | } 494 | hasConcluded = true 495 | // if the timeout already expired, the suite has moved on. 496 | // Doing it again would be a bug. 497 | if (!hasMovedOn) moveOn() 498 | 499 | } 500 | // hops on to the next task after either conclusion or timeout, 501 | // whichever comes first 502 | const moveOn = () => { 503 | hasMovedOn = true 504 | if (isAsync) next() 505 | else nextTickish(next) 506 | } 507 | const startTimer = () => { 508 | timeout = setTimeout(function() { 509 | timeout = undefined 510 | fail(new Assertion().i, "async test timed out after " + delay + "ms", null) 511 | moveOn() 512 | }, Math.min(delay, 0x7fffffff)) 513 | 514 | } 515 | try { 516 | var result = fn(assert) 517 | if (result != null && typeof result.then === 'function') { 518 | // normalize thenables so that we only conclude once 519 | promises.push(Promise.resolve(result)) 520 | } 521 | if (promises.length > 0) { 522 | Promise.all(promises).then( 523 | function() {conclude()}, 524 | function(e) {conclude(e,true)} 525 | ) 526 | isAsync = true 527 | startTimer() 528 | } else { 529 | hasConcluded = true 530 | moveOn() 531 | } 532 | } catch(e) { 533 | conclude(e, true) 534 | } 535 | return 536 | } 537 | // for the legacy API 538 | try { 539 | if (fn.length > 0) { 540 | fn(done, timeoutParamDeprecationNotice) 541 | } else { 542 | var prm = fn() 543 | if (prm && prm.then) { 544 | // Use `_done`, not `finalize` here to defend against badly behaved thenables. 545 | // Let it crash if `then()` doesn't work as expected. 546 | prm.then(function() { _done(null, false) }, function(e) {_done(e, true)}) 547 | } else { 548 | finalize(null, false, false) 549 | } 550 | } 551 | if (!hasMovedOn) { 552 | // done()/_done() haven't been called synchronously 553 | isAsync = true 554 | startTimer() 555 | } 556 | } 557 | catch (e) { 558 | finalize(e, true, false) 559 | } 560 | 561 | // public API, may only be called once from user code (or after the resolution 562 | // of a thenable that's been returned at the end of the test) 563 | function done(err) { 564 | // `!!err` would be more correct as far as node callback go, but we've been 565 | // using a `err != null` test for a while and no one complained... 566 | _done(err, err != null) 567 | } 568 | // common abstraction for node-style callbacks and thenables 569 | function _done(err, threw) { 570 | if (isDone) throw new Error(task.doneTwiceError) 571 | isDone = true 572 | if (isAsync && timeout === undefined) { 573 | $timedOutAndPendingResolution-- 574 | console.warn( 575 | task.context 576 | + "\n# elapsed: " + Math.round(new Date - taskStartTime) 577 | + "ms, expected under " + delay + "ms\n" 578 | + o.cleanStackTrace(task.error)) 579 | } 580 | 581 | 582 | if (!hasMovedOn) finalize(err, threw, false) 583 | } 584 | // called only for async tests 585 | function startTimer() { 586 | timeout = setTimeout(function() { 587 | timeout = undefined 588 | $timedOutAndPendingResolution++ 589 | finalize("async test timed out after " + delay + "ms\nWarning: assertions starting with `???` may not be properly labelled", true, true) 590 | }, Math.min(delay, 0x7fffffff)) 591 | } 592 | // common test finalization code path, for internal use only 593 | function finalize(err, threw, isTimeout) { 594 | if (hasMovedOn) { 595 | // failsafe for hacking, should never happen in released code 596 | throw new Error("Multiple finalization") 597 | } 598 | hasMovedOn = true 599 | 600 | if (threw) { 601 | if (err instanceof Error) fail(new Assertion().i, err.message, err) 602 | else fail(new Assertion().i, String(err), null) 603 | if (!isTimeout) { 604 | $bail() 605 | if (task.hookName === "beforeEach") { 606 | while (!task.internal != null && tasks[cursor].depth > task.depth) cursor++ 607 | } 608 | } 609 | } 610 | if (timeout !== undefined) timeout = clearTimeout(timeout) 611 | 612 | if (isAsync) next() 613 | else nextTickish(next) 614 | } 615 | } 616 | } 617 | } 618 | 619 | // #Assertions 620 | function Assertion(value) { 621 | this.value = value 622 | this.i = results.length 623 | results.push({ 624 | pass: null, 625 | message: "Incomplete assertion in the test definition starting at...", 626 | error: $task.error, 627 | task: $task, 628 | timeoutLimbo: $timedOutAndPendingResolution === 0, 629 | // Deprecated 630 | context: ($timedOutAndPendingResolution === 0 ? "" : "??? ") + $task.context, 631 | testError: $task.error 632 | }) 633 | } 634 | 635 | function plainAssertion(verb, compare) { 636 | return function(self, value) { 637 | var success = compare(self.value, value) 638 | var message = serialize(self.value) + "\n " + verb + "\n" + serialize(value) 639 | if (success) succeed(self.i, message, null) 640 | else fail(self.i, message, null) 641 | } 642 | } 643 | 644 | function define(name, assertion) { 645 | Assertion.prototype[name] = function assert(value) { 646 | var self = this 647 | assertion(self, value) 648 | return function(message) { 649 | if (Array.isArray(message)) { 650 | // We got a tagged template literal, 651 | // we'll interpolate the dynamic values. 652 | var args = arguments 653 | message = message.reduce(function(acc, v, i) {return acc + args[i] + v}) 654 | } 655 | results[self.i].message = message + "\n\n" + results[self.i].message 656 | } 657 | } 658 | } 659 | 660 | define("equals", plainAssertion("should equal", function(a, b) {return a === b})) 661 | define("notEquals", plainAssertion("should not equal", function(a, b) {return a !== b})) 662 | define("deepEquals", plainAssertion("should deep equal", deepEqual)) 663 | define("notDeepEquals", plainAssertion("should not deep equal", function(a, b) {return !deepEqual(a, b)})) 664 | define("throws", plainAssertion("should throw a", throws)) 665 | define("notThrows", plainAssertion("should not throw a", function(a, b) {return !throws(a, b)})) 666 | define("satisfies", function satisfies(self, check) { 667 | try { 668 | var res = check(self.value) 669 | if (res.pass) succeed(self.i, String(res.message), null) 670 | else fail(self.i, String(res.message), null) 671 | } catch (e) { 672 | results.pop() 673 | throw e 674 | } 675 | }) 676 | Assertion.prototype._ = Assertion.prototype.satisfies 677 | define("notSatisfies", function notSatisfies(self, check) { 678 | try { 679 | var res = check(self.value) 680 | if (!res.pass) succeed(self.i, String(res.message), null) 681 | else fail(self.i, String(res.message), null) 682 | } catch (e) { 683 | results.pop() 684 | throw e 685 | } 686 | }) 687 | function isArguments(a) { 688 | if ("callee" in a) { 689 | for (var i in a) if (i === "callee") return false 690 | return true 691 | } 692 | } 693 | function getEnumerableProps(x) { 694 | var desc = Object.getOwnPropertyDescriptors(x) 695 | return Object.keys(desc).filter(function(k){return desc[k].enumerable}) 696 | } 697 | 698 | function deepEqual(a, b) { 699 | if (a === b) return true 700 | if (a === null ^ b === null || a === undefined ^ b === undefined) return false // eslint-disable-line no-bitwise 701 | if (typeof a === "object" && typeof b === "object") { 702 | var aIsArgs = isArguments(a), bIsArgs = isArguments(b) 703 | if (a.constructor === Object && b.constructor === Object && !aIsArgs && !bIsArgs || Object.getPrototypeOf(a) == null && Object.getPrototypeOf(b) == null) { 704 | for (var i in a) { 705 | if ((!(i in b)) || !deepEqual(a[i], b[i])) return false 706 | } 707 | for (var i in b) { 708 | if (!(i in a)) return false 709 | } 710 | return true 711 | } 712 | if (a.length === b.length && (Array.isArray(a) && Array.isArray(b) || aIsArgs && bIsArgs)) { 713 | var aKeys = getEnumerableProps(a), bKeys = getEnumerableProps(b) 714 | if (aKeys.length !== bKeys.length) return false 715 | for (var i = 0; i < aKeys.length; i++) { 716 | if (!hasOwn.call(b, aKeys[i]) || !deepEqual(a[aKeys[i]], b[aKeys[i]])) return false 717 | } 718 | return true 719 | } 720 | if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime() 721 | if (typeof Buffer === "function" && a instanceof Buffer && b instanceof Buffer && a.length === b.length) { 722 | for (var i = 0; i < a.length; i++) { 723 | if (a[i] !== b[i]) return false 724 | } 725 | return true 726 | } 727 | if (typeof a.valueOf !== "function" || typeof b.valueOf !== "function") return false 728 | if (a.valueOf() === b.valueOf()) return true 729 | } 730 | return false 731 | } 732 | 733 | function throws(a, b){ 734 | try{ 735 | a() 736 | }catch(e){ 737 | if(typeof b === "string"){ 738 | return (e.message === b) 739 | }else{ 740 | return (e instanceof b) 741 | } 742 | } 743 | return false 744 | } 745 | 746 | function succeed(i, message, error) { 747 | var result = results[i] 748 | result.pass = true 749 | result.message = message 750 | // for notSatisfies. Use the task.error for other passing assertions 751 | if (error != null) result.error = error 752 | } 753 | 754 | function fail(i, message, error) { 755 | var result = results[i] 756 | result.pass = false 757 | result.message = message 758 | result.error = error != null ? error : ensureStackTrace(new Error) 759 | } 760 | // workaround for Rollup 761 | // direct `require` calles are hoisted at the top of the file 762 | // and ran unconditionally. 763 | 764 | var serialize = function serialize(value) { 765 | if (value === null || (typeof value === "object" && !(value instanceof Array)) || typeof value === "number") return String(value) 766 | else if (typeof value === "function") return value.name || "" 767 | try {return JSON.stringify(value)} catch (e) {return String(value)} 768 | } 769 | try {serialize = require("util").inspect} catch(e) {/* deliberately empty */} // eslint-disable-line global-require 770 | 771 | // o.spy is functionally equivalent to this: 772 | // the extra complexity comes from compatibility issues 773 | // in ES5 environments where you can't overwrite fn.length 774 | 775 | // o.spy = function(fn) { 776 | // var spy = function() { 777 | // spy.this = this 778 | // spy.args = [].slice.call(arguments) 779 | // spy.calls.push({this: this, args: spy.args}) 780 | // spy.callCount++ 781 | 782 | // if (fn) return fn.apply(this, arguments) 783 | // } 784 | // if (fn) 785 | // Object.defineProperties(spy, { 786 | // length: {value: fn.length}, 787 | // name: {value: fn.name} 788 | // }) 789 | // spy.args = [] 790 | // spy.calls = [] 791 | // spy.callCount = 0 792 | // return spy 793 | // } 794 | 795 | var spyFactoryCache = Object.create(null) 796 | 797 | function makeSpyFactory(name, length) { 798 | if (spyFactoryCache[name] == null) spyFactoryCache[name] = [] 799 | var args = Array.apply(null, {length: length}).map( 800 | function(_, i) {return "_" + i} 801 | ).join(", "); 802 | var code = 803 | "'use strict';" + 804 | "var spy = (0, function " + name + "(" + args + ") {" + 805 | " return helper(this, [].slice.call(arguments), fn, spy)" + 806 | "});" + 807 | "return spy" 808 | 809 | return spyFactoryCache[name][length] = new Function("fn", "helper", code) 810 | } 811 | 812 | function getOrMakeSpyFactory(name, length) { 813 | return spyFactoryCache[name] && spyFactoryCache[name][length] || makeSpyFactory(name, length) 814 | } 815 | 816 | function globalSpyHelper(self, args, fn, spy) { 817 | spy.this = self 818 | spy.args = args 819 | spy.calls.push({this: self, args: args}) 820 | spy.callCount++ 821 | 822 | if (fn) return fn.apply(self, args) 823 | } 824 | 825 | var supportsFunctionMutations = false; 826 | // eslint-disable-next-line no-empty, no-implicit-coercion 827 | try {supportsFunctionMutations = !!Object.defineProperties(function(){}, {name: {value: "a"},length: {value: 1}})} catch(_){} 828 | 829 | var supportsEval = false 830 | // eslint-disable-next-line no-new-func, no-empty 831 | try {supportsEval = Function("return true")()} catch(e){} 832 | 833 | function createSpy(helper) { 834 | return function spy(fn) { 835 | var name = "", length = 0 836 | if (fn) name = fn.name, length = fn.length 837 | var spy = (!supportsFunctionMutations && supportsEval) 838 | ? getOrMakeSpyFactory(name, length)(fn, helper) 839 | : function(){return helper(this, [].slice.call(arguments), fn, spy)} 840 | if (supportsFunctionMutations) Object.defineProperties(spy, { 841 | name: {value: name}, 842 | length: {value: length} 843 | }) 844 | 845 | spy.args = [] 846 | spy.calls = [] 847 | spy.callCount = 0 848 | return spy 849 | } 850 | } 851 | 852 | o.spy = createSpy(globalSpyHelper) 853 | 854 | // Reporter 855 | var colorCodes = { 856 | red: "31m", 857 | red2: "31;1m", 858 | green: "32;1m" 859 | } 860 | 861 | // this is needed to work around the formating done by node see https://nodejs.org/api/util.html#utilformatformat-args 862 | function escapePercent(x){return String(x).replace(/%/g, "%%")} 863 | 864 | // console style for terminals 865 | // see https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences 866 | function highlight(message, color) { 867 | var code = colorCodes[color] || colorCodes.red; 868 | return hasProcess ? (process.stdout.isTTY ? "\x1b[" + code + escapePercent(message) + "\x1b[0m" : escapePercent(message)) : "%c" + message + "%c " 869 | } 870 | 871 | // console style for the Browsers 872 | // see https://developer.mozilla.org/en-US/docs/Web/API/console#Styling_console_output 873 | function cStyle(color, bold) { 874 | return hasProcess||!color ? "" : "color:"+color+(bold ? ";font-weight:bold" : "") 875 | } 876 | 877 | function onlyWarning(onlyCalledAt) { 878 | var colors = Math.random() > 0.5 879 | ? { 880 | term: "red2", 881 | web: cStyle("red", true) 882 | } 883 | : { 884 | term: "re", 885 | web: cStyle("red") 886 | } 887 | if (onlyCalledAt && onlyCalledAt.length !== 0) { 888 | console.warn( 889 | highlight("\nWarning: o.only() called...\n", colors.term), 890 | colors.web, "" 891 | ) 892 | console.warn(onlyCalledAt.join("\n")) 893 | console.warn( 894 | highlight("\nWarning: o.only()\n", colors.term), 895 | colors.web, "" 896 | ) 897 | } 898 | } 899 | 900 | o.report = function (results, stats) { 901 | if (stats == null) stats = {bailCount: 0} 902 | var errCount = -stats.bailCount 903 | for (var i = 0, r; r = results[i]; i++) { 904 | if (!r.pass) { 905 | var stackTrace = o.cleanStackTrace(r.error) 906 | var couldHaveABetterStackTrace = !stackTrace || timeoutStackName != null && stackTrace.indexOf(timeoutStackName) !== -1 && stackTrace.indexOf("\n") === -1 907 | if (couldHaveABetterStackTrace) stackTrace = r.task.error != null ? o.cleanStackTrace(r.task.error) : r.error.stack || "" 908 | console.error( 909 | (hasProcess ? "\n" : "") + 910 | (r.task.timeoutLimbo ? "??? " : "") + 911 | highlight(r.task.context + ":", "red2") + "\n" + 912 | highlight(r.message, "red") + 913 | (stackTrace ? "\n" + stackTrace + "\n" : ""), 914 | 915 | cStyle("black", true), cStyle(null), // reset to default 916 | cStyle("red"), cStyle("black") 917 | ) 918 | errCount++ 919 | } 920 | } 921 | var pl = results.length === 1 ? "" : "s" 922 | 923 | var total = results.length - stats.bailCount 924 | var message = [], log = [] 925 | 926 | if (hasProcess) message.push("––––––\n") 927 | 928 | if (name) message.push(name + ": ") 929 | 930 | if (errCount === 0 && stats.bailCount === 0) { 931 | message.push(highlight((pl ? "All " : "The ") + total + " assertion" + pl + " passed", "green")) 932 | log.push(cStyle("green" , true), cStyle(null)) 933 | } else if (errCount === 0) { 934 | message.push((pl ? "All " : "The ") + total + " assertion" + pl + " passed") 935 | } else { 936 | message.push(highlight(errCount + " out of " + total + " assertion" + pl + " failed", "red2")) 937 | log.push(cStyle("red" , true), cStyle(null)) 938 | } 939 | 940 | if (stats.bailCount !== 0) { 941 | message.push(highlight(". Bailed out " + stats.bailCount + (stats.bailCount === 1 ? " time" : " times"), "red")) 942 | log.push(cStyle("red"), cStyle(null)) 943 | } 944 | 945 | log.unshift(message.join("")) 946 | console.log.apply(console, log) 947 | 948 | onlyWarning(stats.onlyCalledAt) 949 | 950 | return errCount + stats.bailCount 951 | } 952 | return o 953 | }) 954 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ospec", 3 | "version": "4.2.1", 4 | "description": "Noiseless testing framework", 5 | "main": "ospec.js", 6 | "unpkg": "ospec.js", 7 | "keywords": [ 8 | "testing" 9 | ], 10 | "author": "Leo Horie ", 11 | "license": "MIT", 12 | "files": [ 13 | "bin", 14 | "ospec.js", 15 | "scripts/rename-stable-binaries.js" 16 | ], 17 | "bin": "./bin/ospec", 18 | "repository": "github:MithrilJS/ospec", 19 | "dependencies": { 20 | "glob": "^9.0.0" 21 | }, 22 | "scripts": { 23 | "postinstall": "node ./scripts/rename-stable-binaries.js", 24 | "test": "ospec-stable tests/test-*.js", 25 | "test-api": "ospec-stable tests/test-api-*.js", 26 | "test-cli": "ospec-stable tests/test-cli.js", 27 | "self-test": "node ./bin/ospec tests/test-*.js", 28 | "self-test-api": "node ./bin/ospec tests/test-api-*.js", 29 | "self-test-cli": "node ./bin/ospec tests/test-cli.js", 30 | "lint": "eslint --cache --ignore-pattern \"tests/fixtures/**/*.*\" . bin/ospec", 31 | "lint-fix": "eslint --cache --ignore-pattern \"tests/fixtures/**/*.*\" --fix . bin/ospec" 32 | }, 33 | "devDependencies": { 34 | "cmd-shim": "4.0.2", 35 | "compose-regexp": "^0.6.22", 36 | "eslint": "^6.8.0", 37 | "ospec-stable": "npm:ospec@4.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing ospec 2 | 3 | This was originally tied to the Mithril release cycle, of which nothing remains. 4 | 5 | Currently, the process is manual: 6 | 7 | 1. check that the test suite passes, both locally and in the GH actions (bar some timeout flakiness) 8 | 2. check that we're at the current `main` tip; if not, check it out and goto 1. 9 | 3. check that the git tree is clean; if not, check it out and goto 1. 10 | 4. verify that the change log is up to date. Update it if needed. 11 | 5. verify that the readme is up to date. Update it if needed (check the LOC stats and the API docs). 12 | 6. bump the version number in `package.json`. 13 | 7. commit, tag and push. 14 | 8. npm publish 15 | 16 | 9. 17 | ``` 18 | \o/ 19 | | 20 | / \ 21 | ``` 22 | 23 | 10. Bump ospec-stable in `package.json` and run the test suite. -------------------------------------------------------------------------------- /scripts/build-done-parser.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const {sequence, either, suffix, capture} = require("compose-regexp") 4 | const zeroOrMore = suffix("*") 5 | const maybe = suffix("?") 6 | 7 | const any = /[^]/ 8 | const space = /\s/ 9 | 10 | const multiLineComment = sequence("/*", suffix("*?", any), "*/") 11 | 12 | // |$ not needed at the end since this is the 13 | // start of a function that has one parameter 14 | // and we don't scan past said parameter 15 | // there is necessrily more text following 16 | // a comment that occurs in this scenario 17 | const singleLineComment = sequence("//", zeroOrMore(/[^\n]/), "\n") 18 | 19 | const ignorable = suffix("*", either(space, multiLineComment, singleLineComment)) 20 | 21 | // very loose definitions here, knowing that we're matching valid JS 22 | // space, '(' for the start of the args, '/' for a comment 23 | const funcName = /[^\s(\/]+/ 24 | // space, '[' and '{' for destructuring, ')' for the end of args, ',' for next argument, 25 | // '=' for => and '/' for comments 26 | const argName = /[^\s{[),=\/]+/ 27 | 28 | const prologue = (____) => maybe( 29 | maybe("function", ____, maybe(/\b/, funcName, ____)), 30 | "(", 31 | ____ 32 | ) 33 | 34 | // This captures the first identifier after skipping 35 | // (?:(?:function identifier?)? \()? 36 | const doneMatcher = sequence(/^/, prologue(ignorable), capture(argName)) 37 | 38 | console.log("without comments: ", sequence(/^/, prologue(/ */), capture(argName))) 39 | 40 | // ------------------------------------------------------------ // 41 | // tests and output if all green 42 | 43 | const tests = [ 44 | ["function(done)", "done"], 45 | ["function (done)", "done"], 46 | ["function foo(done)", "done"], 47 | ["function foo (done)", "done"], 48 | [`function /**/ /* foo */ //hoho 49 | /* 50 | bar */ 51 | //baz 52 | (done)`, "done"], 53 | [`function/**/ /* foo */ //hoho 54 | /* 55 | bar */ 56 | //baz 57 | foo(done)`, "done"], 58 | [`function/**/ /* foo */ //hoho 59 | /* 60 | bar */ 61 | //baz 62 | foo/**/ /* foo */ //hoho 63 | /* 64 | bar */ 65 | //baz 66 | (done)`, "done"], 67 | ["function(done, timeout)", "done"], 68 | ["function (done, timeout)", "done"], 69 | ["function foo(done, timeout)", "done"], 70 | ["function foo (done, timeout)", "done"], 71 | ["function( done, timeout)", "done"], 72 | ["function ( done, timeout)", "done"], 73 | ["function foo( done, timeout)", "done"], 74 | ["function foo ( done, timeout)", "done"], 75 | [`function /**/ /* foo */ //hoho 76 | /* 77 | bar */ 78 | //baz 79 | ( done, timeout)`, "done"], 80 | [`function/**/ /* foo */ //hoho 81 | /* 82 | bar */ 83 | //baz 84 | foo(done, timeout)`, "done"], 85 | [`function/**/ /* foo */ //hoho 86 | /* 87 | bar */ 88 | //baz 89 | foo/**/ /* foo */ //hoho 90 | /* 91 | bar */ 92 | //baz 93 | (/**/ /* foo */ //hoho 94 | /* 95 | bar */ 96 | //baz 97 | done, timeout)`, "done"], 98 | ["( done ) => ", "done"], 99 | ["( done/**/, define) => ", "done"], 100 | ["( done, define) => ", "done"], 101 | ["( done , define) => ", "done"], 102 | [`(//foo 103 | /* 104 | */done//more comment 105 | /* and then some 106 | */) => `, "done"], 107 | ["done =>", "done"], 108 | ["done=>", "done"], 109 | ["done /* foo */=>", "done"], 110 | ["done/* foo */ =>", "done"], 111 | ["done /* foo */ /*bar*/ =>", "done"], 112 | ["function$dada =>", "function$dada"], 113 | ["(function$dada) =>", "function$dada"], 114 | ["function(function$dada) {", "function$dada"], 115 | ] 116 | 117 | 118 | let ok = true; 119 | 120 | tests.forEach(([candidate, expected]) => { 121 | if ((doneMatcher.exec(candidate)||[]).pop() !== expected) { 122 | ok = false 123 | console.log( 124 | `parsing \n\n\t${ 125 | candidate 126 | }\n\nresulted in ${ 127 | JSON.stringify(((doneMatcher.exec(candidate)||[]).pop())) 128 | }, not ${ 129 | JSON.stringify(expected) 130 | } as expected\n` 131 | ) 132 | } 133 | }) 134 | 135 | if (ok) console.log(`Paste this:\n\n${doneMatcher}\n`) 136 | -------------------------------------------------------------------------------- /scripts/logger.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | // TODO: properly document this 4 | 5 | const o = require("ospec") 6 | const {report} = 0 7 | 8 | const {writeFileSync, mkdirSync} = require("node:fs") 9 | const {join} = require("node:path") 10 | 11 | console.log("Logging...") 12 | mkdirSync("./logs", {recursive: true}) 13 | 14 | function toPlain({message, stack}) { 15 | return {message, stack} 16 | } 17 | 18 | o.report = function(results) { 19 | const path = join(".", "logs", `${results.length}-${String(Date.now())}.json`) 20 | writeFileSync(path, JSON.stringify(results.map( 21 | (r) => { 22 | r.error=toPlain(r.error) 23 | r.testError = toPlain(r.testError) 24 | return r 25 | } 26 | ), null, 2)) 27 | console.log(`results written to ${path}`) 28 | return report(results) 29 | } 30 | -------------------------------------------------------------------------------- /scripts/rename-stable-binaries.js: -------------------------------------------------------------------------------- 1 | // This is a dev script that is shipped with the package because 2 | // I couldn't find a cross-platform way of running code conditionally 3 | // The shell syntaxes are too complex. 4 | "use strict" 5 | 6 | // eslint-disable no-process-exit 7 | 8 | const {rename} = require("node:fs/promises") 9 | const glob = require("glob") 10 | 11 | let count = 0 12 | 13 | glob.globStream("node_modules/.bin/ospec*(.*)") 14 | 15 | .on("data", (x) => {console.log(x); count++; rename(x, x.replace(/ospec(?:-stable)?((?:\.\w+)?)/, "ospec-stable$1"))}) 16 | 17 | .on("error", (e) => { 18 | throw e 19 | }) 20 | 21 | .on("end", () => {if (count !== 0) console.log(`We renamed ${count} file${count > 1 ? "s" : ""}`)}) 22 | 23 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/README.md: -------------------------------------------------------------------------------- 1 | ## Unit and integration tests 2 | 3 | ## End to end tests 4 | 5 | The `fixtures/*/*/{esm, cjs}` subdiretcories contain pseudo-projects with the current code base defined as the `ospec` dependency, and js files in various directories. 6 | 7 | Each `fixtures/{legacy|v5}/*` subdirectory contains a `config.js` file that is used to generate the pseudo-projects. 8 | 9 | The `config` exports a `"package.json"` field and a `"js"` field that contains nested objects describing the layout of the JS files, and their content. 10 | 11 | While each directory has its peculiarirties (described hereafter), here is the common structure (after node_modules has been initialized): 12 | 13 | ```JS 14 | [ 15 | { 16 | "explicit" : [ 17 | "explicit1.js", 18 | "explicit2.js", 19 | ] 20 | }, 21 | "main.js", 22 | { 23 | "node_modules": { 24 | ".bin": ["ospec" /*, ospec.cmd */], 25 | "ospec": [ 26 | { bin: ["ospec"] }, 27 | "ospec.js", 28 | "package.json" 29 | ], 30 | "dummy-package-with-tests": [ 31 | "package.json", 32 | { tests: ["should-not-run.js"] } 33 | ] 34 | } 35 | }, 36 | "other.js", 37 | "package.json", 38 | { 39 | tests: ["main1.js", "main2.js"] 40 | very: { deep: { tests: [ 41 | "deep1.js", 42 | { deeper: ["deep2.js"] } 43 | ]}} 44 | } 45 | ] 46 | ``` 47 | 48 | - *success* has all the test files succeeding 49 | - *throws* sees every file throw errors just after printing that they ran. 50 | - TODO *failure* has all the test files filled with failing assertions 51 | - TODO *lone-failure* has all but one assertion that succeeds in each directory 52 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/metadata/cjs/default1.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(__filename + " ran") 5 | 6 | const o = require("ospec") 7 | o.globalAssertions("override") 8 | 9 | 10 | o.spec(__filename, function() { 11 | 12 | o("test", function() { 13 | const md = o.metadata() 14 | console.log(md.file + " metadata file from test") 15 | console.log(md.name + " metadata name from test") 16 | o().satisfies(function() { 17 | const md = o.metadata() 18 | console.log(md.file + " metadata file from assertion") 19 | console.log(md.name + " metadata name from assertion") 20 | return {pass: true} 21 | }) 22 | }) 23 | 24 | }) -------------------------------------------------------------------------------- /tests/fixtures/legacy/metadata/cjs/default2.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(__filename + " ran") 5 | 6 | const o = require("ospec") 7 | o.globalAssertions("override") 8 | 9 | 10 | o.spec(__filename, function() { 11 | 12 | o("test", function() { 13 | const md = o.metadata() 14 | console.log(md.file + " metadata file from test") 15 | console.log(md.name + " metadata name from test") 16 | o().satisfies(function() { 17 | const md = o.metadata() 18 | console.log(md.file + " metadata file from assertion") 19 | console.log(md.name + " metadata name from assertion") 20 | return {pass: true} 21 | }) 22 | }) 23 | 24 | }) -------------------------------------------------------------------------------- /tests/fixtures/legacy/metadata/cjs/override.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(__filename + " ran") 5 | 6 | const o = require("ospec") 7 | o.globalAssertions("override") 8 | 9 | 10 | o.metadata({file: "foo"}) 11 | 12 | o.spec(__filename, function() { 13 | 14 | o("test", function() { 15 | const md = o.metadata() 16 | console.log(md.file + " metadata file from test") 17 | console.log(md.name + " metadata name from test") 18 | o().satisfies(function() { 19 | const md = o.metadata() 20 | console.log(md.file + " metadata file from assertion") 21 | console.log(md.name + " metadata name from assertion") 22 | return {pass: true} 23 | }) 24 | }) 25 | 26 | }) -------------------------------------------------------------------------------- /tests/fixtures/legacy/metadata/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "metadata": "ospec default1.js default2.js override.js", 5 | "which": "which ospec" 6 | } 7 | } -------------------------------------------------------------------------------- /tests/fixtures/legacy/metadata/config.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | exports["package.json"] = { 4 | "license": "ISC", 5 | "scripts": { 6 | "metadata": "ospec default1.js default2.js override.js", 7 | "which" : "which ospec" 8 | } 9 | } 10 | 11 | const isWindows = process.platform === "win32" 12 | 13 | const cjsFileName = "__filename" 14 | const esmFileName = 15 | isWindows ? String.raw`import.meta.url.slice(8).replace(/\//g, '\\')`: 16 | "import.meta.url.slice(7)" 17 | 18 | 19 | const cjsHeader = ` 20 | "use strict" 21 | console.log(${cjsFileName} + " ran") 22 | 23 | const o = require("ospec") 24 | o.globalAssertions("override") 25 | ` 26 | 27 | const esmHeader = ` 28 | "use strict" 29 | console.log(${esmFileName} + " ran") 30 | 31 | import {default as o} from 'ospec' 32 | o.globalAssertions("override") 33 | ` 34 | 35 | const override = ` 36 | o.metadata({file: "foo"}) 37 | ` 38 | 39 | const test = ` 40 | o("test", function() { 41 | const md = o.metadata() 42 | console.log(md.file + " metadata file from test") 43 | console.log(md.name + " metadata name from test") 44 | o().satisfies(function() { 45 | const md = o.metadata() 46 | console.log(md.file + " metadata file from assertion") 47 | console.log(md.name + " metadata name from assertion") 48 | return {pass: true} 49 | }) 50 | }) 51 | ` 52 | 53 | const file = (header, middle, filename) => ` 54 | ${header} 55 | ${middle} 56 | o.spec(${filename}, function() { 57 | ${test} 58 | })` 59 | 60 | const defaultMd = { 61 | cjs: file(cjsHeader, "", cjsFileName), 62 | esm: file(esmHeader, "", esmFileName) 63 | } 64 | const overrideMd = { 65 | cjs: file(cjsHeader, override, cjsFileName), 66 | esm: file(esmHeader, override, esmFileName) 67 | } 68 | exports["js"] = { 69 | "default1.js": defaultMd, 70 | "default2.js": defaultMd, 71 | "override.js": overrideMd, 72 | } 73 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/metadata/esm/default1.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(import.meta.url.slice(7) + " ran") 5 | 6 | import {default as o} from 'ospec' 7 | o.globalAssertions("override") 8 | 9 | 10 | o.spec(import.meta.url.slice(7), function() { 11 | 12 | o("test", function() { 13 | const md = o.metadata() 14 | console.log(md.file + " metadata file from test") 15 | console.log(md.name + " metadata name from test") 16 | o().satisfies(function() { 17 | const md = o.metadata() 18 | console.log(md.file + " metadata file from assertion") 19 | console.log(md.name + " metadata name from assertion") 20 | return {pass: true} 21 | }) 22 | }) 23 | 24 | }) -------------------------------------------------------------------------------- /tests/fixtures/legacy/metadata/esm/default2.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(import.meta.url.slice(7) + " ran") 5 | 6 | import {default as o} from 'ospec' 7 | o.globalAssertions("override") 8 | 9 | 10 | o.spec(import.meta.url.slice(7), function() { 11 | 12 | o("test", function() { 13 | const md = o.metadata() 14 | console.log(md.file + " metadata file from test") 15 | console.log(md.name + " metadata name from test") 16 | o().satisfies(function() { 17 | const md = o.metadata() 18 | console.log(md.file + " metadata file from assertion") 19 | console.log(md.name + " metadata name from assertion") 20 | return {pass: true} 21 | }) 22 | }) 23 | 24 | }) -------------------------------------------------------------------------------- /tests/fixtures/legacy/metadata/esm/override.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(import.meta.url.slice(7) + " ran") 5 | 6 | import {default as o} from 'ospec' 7 | o.globalAssertions("override") 8 | 9 | 10 | o.metadata({file: "foo"}) 11 | 12 | o.spec(import.meta.url.slice(7), function() { 13 | 14 | o("test", function() { 15 | const md = o.metadata() 16 | console.log(md.file + " metadata file from test") 17 | console.log(md.name + " metadata name from test") 18 | o().satisfies(function() { 19 | const md = o.metadata() 20 | console.log(md.file + " metadata file from assertion") 21 | console.log(md.name + " metadata name from assertion") 22 | return {pass: true} 23 | }) 24 | }) 25 | 26 | }) -------------------------------------------------------------------------------- /tests/fixtures/legacy/metadata/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "metadata": "ospec default1.js default2.js override.js", 5 | "which": "which ospec" 6 | }, 7 | "type": "module" 8 | } -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/cjs/explicit/explicit1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | 7 | o.globalAssertions("override") 8 | 9 | o(__filename, () => { 10 | console.log(__filename + " had tests") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/cjs/explicit/explicit2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | 7 | o.globalAssertions("override") 8 | 9 | o(__filename, () => { 10 | console.log(__filename + " had tests") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/cjs/main.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/cjs/other.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "default": "ospec", 5 | "explicit-one": "ospec ./explicit/explicit1.js", 6 | "explicit-several": "ospec ./explicit/explicit1.js ./explicit/explicit2.js", 7 | "explicit-glob": "ospec \"explicit/*.js\"", 8 | "ignore-one": "ospec --ignore tests/main2.js", 9 | "ignore-one-glob": "ospec --ignore \"very/**/*.js\"", 10 | "ignore-several": "ospec --ignore \"very/**\" --ignore tests/main2.js", 11 | "preload-one": "ospec --preload ./main.js", 12 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 13 | "require-one": "ospec --require ./main.js", 14 | "require-several": "ospec --require ./main.js --require ./other.js", 15 | "which": "which ospec" 16 | } 17 | } -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/cjs/tests/main1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | 7 | o.globalAssertions("override") 8 | 9 | o(__filename, () => { 10 | console.log(__filename + " had tests") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/cjs/tests/main2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | 7 | o.globalAssertions("override") 8 | 9 | o(__filename, () => { 10 | console.log(__filename + " had tests") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/cjs/very/deep/tests/deep1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | 7 | o.globalAssertions("override") 8 | 9 | o(__filename, () => { 10 | console.log(__filename + " had tests") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/cjs/very/deep/tests/deeper/deep2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | 7 | o.globalAssertions("override") 8 | 9 | o(__filename, () => { 10 | console.log(__filename + " had tests") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/config.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | exports["package.json"] = { 4 | "license": "ISC", 5 | "scripts": { 6 | "default": "ospec", 7 | "explicit-one": "ospec ./explicit/explicit1.js", 8 | "explicit-several": "ospec ./explicit/explicit1.js ./explicit/explicit2.js", 9 | "explicit-glob": "ospec \"explicit/*.js\"", 10 | // TODO investigate why --ignore is so capricious 11 | // `tests/test2.js` works, but `./tests/test2.js` doesn't. 12 | "ignore-one": "ospec --ignore tests/main2.js", 13 | "ignore-one-glob": "ospec --ignore \"very/**/*.js\"", 14 | "ignore-several": "ospec --ignore \"very/**\" --ignore tests/main2.js", 15 | "preload-one": "ospec --preload ./main.js", 16 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 17 | "require-one": "ospec --require ./main.js", 18 | "require-several": "ospec --require ./main.js --require ./other.js", 19 | "which" : "which ospec" 20 | } 21 | } 22 | 23 | const noTest = { 24 | cjs: ` 25 | "use strict" 26 | console.log(__filename + " ran") 27 | `, 28 | esm: ` 29 | "use strict" 30 | console.log(import.meta.url.slice(7) + " ran") 31 | 32 | `} 33 | 34 | const withTest = { 35 | cjs: ` 36 | "use strict" 37 | console.log(__filename + " ran") 38 | 39 | const o = require("ospec") 40 | 41 | o.globalAssertions("override") 42 | 43 | o(__filename, () => { 44 | console.log(__filename + " had tests") 45 | o(true).equals(true) 46 | o(true).equals(true) 47 | }) 48 | `, 49 | esm: ` 50 | "use strict" 51 | console.log(import.meta.url.slice(7) + " ran") 52 | 53 | import {default as o} from 'ospec' 54 | 55 | o.globalAssertions("override") 56 | 57 | o(import.meta.url.slice(7), () => { 58 | console.log(import.meta.url.slice(7) + " ran") 59 | o(true).equals(true) 60 | o(true).equals(true) 61 | }) 62 | `} 63 | 64 | exports["js"] = { 65 | explicit: { 66 | "explicit1.js": withTest, 67 | "explicit2.js": withTest, 68 | }, 69 | "main.js": noTest, 70 | "other.js": noTest, 71 | tests: { 72 | "main1.js": withTest, 73 | "main2.js": withTest, 74 | }, 75 | very: { 76 | deep: { 77 | "tests" : { 78 | "deep1.js": withTest, 79 | deeper: { 80 | "deep2.js": withTest 81 | } 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/esm/explicit/explicit1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | 7 | o.globalAssertions("override") 8 | 9 | o(import.meta.url.slice(7), () => { 10 | console.log(import.meta.url.slice(7) + " ran") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/esm/explicit/explicit2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | 7 | o.globalAssertions("override") 8 | 9 | o(import.meta.url.slice(7), () => { 10 | console.log(import.meta.url.slice(7) + " ran") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/esm/main.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/esm/other.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "default": "ospec", 5 | "explicit-one": "ospec ./explicit/explicit1.js", 6 | "explicit-several": "ospec ./explicit/explicit1.js ./explicit/explicit2.js", 7 | "explicit-glob": "ospec \"explicit/*.js\"", 8 | "ignore-one": "ospec --ignore tests/main2.js", 9 | "ignore-one-glob": "ospec --ignore \"very/**/*.js\"", 10 | "ignore-several": "ospec --ignore \"very/**\" --ignore tests/main2.js", 11 | "preload-one": "ospec --preload ./main.js", 12 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 13 | "require-one": "ospec --require ./main.js", 14 | "require-several": "ospec --require ./main.js --require ./other.js", 15 | "which": "which ospec" 16 | }, 17 | "type": "module" 18 | } -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/esm/tests/main1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | 7 | o.globalAssertions("override") 8 | 9 | o(import.meta.url.slice(7), () => { 10 | console.log(import.meta.url.slice(7) + " ran") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/esm/tests/main2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | 7 | o.globalAssertions("override") 8 | 9 | o(import.meta.url.slice(7), () => { 10 | console.log(import.meta.url.slice(7) + " ran") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/esm/very/deep/tests/deep1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | 7 | o.globalAssertions("override") 8 | 9 | o(import.meta.url.slice(7), () => { 10 | console.log(import.meta.url.slice(7) + " ran") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/success/esm/very/deep/tests/deeper/deep2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | 7 | o.globalAssertions("override") 8 | 9 | o(import.meta.url.slice(7), () => { 10 | console.log(import.meta.url.slice(7) + " ran") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/cjs/main.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | throw __filename + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/cjs/other.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "default": "ospec", 5 | "preload-one": "ospec --preload ./main.js", 6 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 7 | "require-one": "ospec --require ./main.js", 8 | "require-several": "ospec --require ./main.js --require ./other.js", 9 | "which": "which ospec" 10 | } 11 | } -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/cjs/tests/main1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/cjs/tests/main2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | throw __filename + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/cjs/very/deep/tests/deep1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | throw __filename + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/cjs/very/deep/tests/deeper/deep2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | 7 | o.globalAssertions("override") 8 | 9 | o(__filename, () => { 10 | console.log(__filename + " had tests") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/config.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | exports["package.json"] = { 4 | "license": "ISC", 5 | "scripts": { 6 | "default": "ospec", 7 | "preload-one": "ospec --preload ./main.js", 8 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 9 | "require-one": "ospec --require ./main.js", 10 | "require-several": "ospec --require ./main.js --require ./other.js", 11 | "which" : "which ospec" 12 | } 13 | } 14 | 15 | const throws = { 16 | cjs: ` 17 | "use strict" 18 | console.log(__filename + " ran") 19 | throw __filename + " threw" 20 | `, 21 | esm: ` 22 | "use strict" 23 | console.log(import.meta.url.slice(7) + " ran") 24 | throw import.meta.url.slice(7) + " threw" 25 | `} 26 | 27 | const noThrowNoTest = { 28 | cjs: ` 29 | "use strict" 30 | console.log(__filename + " ran") 31 | `, 32 | esm: ` 33 | "use strict" 34 | console.log(import.meta.url.slice(7) + " ran") 35 | `} 36 | 37 | const noThrowWithTest = { 38 | cjs: ` 39 | "use strict" 40 | console.log(__filename + " ran") 41 | 42 | const o = require("ospec") 43 | 44 | o.globalAssertions("override") 45 | 46 | o(__filename, () => { 47 | console.log(__filename + " had tests") 48 | o(true).equals(true) 49 | o(true).equals(true) 50 | }) 51 | `, 52 | esm: ` 53 | "use strict" 54 | console.log(import.meta.url.slice(7) + " ran") 55 | 56 | import {default as o} from 'ospec' 57 | 58 | o.globalAssertions("override") 59 | 60 | o(import.meta.url.slice(7), () => { 61 | console.log(import.meta.url.slice(7) + " ran") 62 | o(true).equals(true) 63 | o(true).equals(true) 64 | }) 65 | `} 66 | 67 | exports["js"] = { 68 | "main.js": throws, 69 | "other.js": noThrowNoTest, 70 | tests: { 71 | "main1.js": noThrowNoTest, 72 | "main2.js": throws, 73 | }, 74 | very: { 75 | deep: { 76 | "tests" : { 77 | "deep1.js": throws, 78 | deeper: { 79 | "deep2.js": noThrowWithTest 80 | } 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/esm/main.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | throw import.meta.url.slice(7) + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/esm/other.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "default": "ospec", 5 | "preload-one": "ospec --preload ./main.js", 6 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 7 | "require-one": "ospec --require ./main.js", 8 | "require-several": "ospec --require ./main.js --require ./other.js", 9 | "which": "which ospec" 10 | }, 11 | "type": "module" 12 | } -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/esm/tests/main1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/esm/tests/main2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | throw import.meta.url.slice(7) + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/esm/very/deep/tests/deep1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | throw import.meta.url.slice(7) + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/legacy/throws/esm/very/deep/tests/deeper/deep2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | 7 | o.globalAssertions("override") 8 | 9 | o(import.meta.url.slice(7), () => { 10 | console.log(import.meta.url.slice(7) + " ran") 11 | o(true).equals(true) 12 | o(true).equals(true) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/fixtures/v5/README.md: -------------------------------------------------------------------------------- 1 | ## Unit and integration tests 2 | 3 | ## End to end tests 4 | 5 | The `fixtures/*/*/{esm, cjs}` subdiretcories contain pseudo-projects with the current code base defined as the `ospec` dependency, and js files in various directories. 6 | 7 | Each `fixtures/{legacy|v5}/*` subdirectory contains a `config.js` file that is used to generate the pseudo-projects. 8 | 9 | The `config` exports a `"package.json"` field and a `"js"` field that contains nested objects describing the layout of the JS files, and their content. 10 | 11 | While each directory has its peculiarirties (described hereafter), here is the common structure (after node_modules has been initialized): 12 | 13 | ```JS 14 | [ 15 | { 16 | "explicit" : [ 17 | "explicit1.js", 18 | "explicit2.js", 19 | ] 20 | }, 21 | "main.js", 22 | { 23 | "node_modules": { 24 | ".bin": ["ospec" /*, ospec.cmd */], 25 | "ospec": [ 26 | { bin: ["ospec"] }, 27 | "ospec.js", 28 | "package.json" 29 | ], 30 | "dummy-package-with-tests": [ 31 | "package.json", 32 | { tests: ["should-not-run.js"] } 33 | ] 34 | } 35 | }, 36 | "other.js", 37 | "package.json", 38 | { 39 | tests: ["main1.js", "main2.js"] 40 | very: { deep: { tests: [ 41 | "deep1.js", 42 | { deeper: ["deep2.js"] } 43 | ]}} 44 | } 45 | ] 46 | ``` 47 | 48 | - *success* has all the test files succeeding 49 | - *throws* sees every file throw errors just after printing that they ran. 50 | - TODO *failure* has all the test files filled with failing assertions 51 | - TODO *lone-failure* has all but one assertion that succeeds in each directory 52 | -------------------------------------------------------------------------------- /tests/fixtures/v5/metadata/cjs/default1.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(__filename + " ran") 5 | 6 | const o = require("ospec") 7 | o.localAssertions("override") 8 | 9 | 10 | o.spec(__filename, function() { 11 | 12 | o("test", function(o) { 13 | const md = o.metadata() 14 | console.log(md.file + " metadata file from test") 15 | console.log(md.name + " metadata name from test") 16 | o().satisfies(function() { 17 | const md = o.metadata() 18 | console.log(md.file + " metadata file from assertion") 19 | console.log(md.name + " metadata name from assertion") 20 | return {pass: true} 21 | }) 22 | }) 23 | 24 | }) -------------------------------------------------------------------------------- /tests/fixtures/v5/metadata/cjs/default2.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(__filename + " ran") 5 | 6 | const o = require("ospec") 7 | o.localAssertions("override") 8 | 9 | 10 | o.spec(__filename, function() { 11 | 12 | o("test", function(o) { 13 | const md = o.metadata() 14 | console.log(md.file + " metadata file from test") 15 | console.log(md.name + " metadata name from test") 16 | o().satisfies(function() { 17 | const md = o.metadata() 18 | console.log(md.file + " metadata file from assertion") 19 | console.log(md.name + " metadata name from assertion") 20 | return {pass: true} 21 | }) 22 | }) 23 | 24 | }) -------------------------------------------------------------------------------- /tests/fixtures/v5/metadata/cjs/override.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(__filename + " ran") 5 | 6 | const o = require("ospec") 7 | o.localAssertions("override") 8 | 9 | 10 | o.metadata({file: "foo"}) 11 | 12 | o.spec(__filename, function() { 13 | 14 | o("test", function(o) { 15 | const md = o.metadata() 16 | console.log(md.file + " metadata file from test") 17 | console.log(md.name + " metadata name from test") 18 | o().satisfies(function() { 19 | const md = o.metadata() 20 | console.log(md.file + " metadata file from assertion") 21 | console.log(md.name + " metadata name from assertion") 22 | return {pass: true} 23 | }) 24 | }) 25 | 26 | }) -------------------------------------------------------------------------------- /tests/fixtures/v5/metadata/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "metadata": "ospec default1.js default2.js override.js", 5 | "which": "which ospec" 6 | } 7 | } -------------------------------------------------------------------------------- /tests/fixtures/v5/metadata/config.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | exports["package.json"] = { 4 | "license": "ISC", 5 | "scripts": { 6 | "metadata": "ospec default1.js default2.js override.js", 7 | "which" : "which ospec" 8 | } 9 | } 10 | 11 | const isWindows = process.platform === "win32" 12 | 13 | const cjsFileName = "__filename" 14 | const esmFileName = 15 | isWindows ? String.raw`import.meta.url.slice(8).replace(/\//g, '\\')`: 16 | "import.meta.url.slice(7)" 17 | 18 | 19 | const cjsHeader = ` 20 | "use strict" 21 | console.log(${cjsFileName} + " ran") 22 | 23 | const o = require("ospec") 24 | o.localAssertions("override") 25 | ` 26 | 27 | const esmHeader = ` 28 | "use strict" 29 | console.log(${esmFileName} + " ran") 30 | 31 | import {default as o} from 'ospec' 32 | o.localAssertions("override") 33 | ` 34 | 35 | const override = ` 36 | o.metadata({file: "foo"}) 37 | ` 38 | 39 | const test = ` 40 | o("test", function(o) { 41 | const md = o.metadata() 42 | console.log(md.file + " metadata file from test") 43 | console.log(md.name + " metadata name from test") 44 | o().satisfies(function() { 45 | const md = o.metadata() 46 | console.log(md.file + " metadata file from assertion") 47 | console.log(md.name + " metadata name from assertion") 48 | return {pass: true} 49 | }) 50 | }) 51 | ` 52 | 53 | const file = (header, middle, filename) => ` 54 | ${header} 55 | ${middle} 56 | o.spec(${filename}, function() { 57 | ${test} 58 | })` 59 | 60 | const defaultMd = { 61 | cjs: file(cjsHeader, "", cjsFileName), 62 | esm: file(esmHeader, "", esmFileName) 63 | } 64 | const overrideMd = { 65 | cjs: file(cjsHeader, override, cjsFileName), 66 | esm: file(esmHeader, override, esmFileName) 67 | } 68 | exports["js"] = { 69 | "default1.js": defaultMd, 70 | "default2.js": defaultMd, 71 | "override.js": overrideMd, 72 | } 73 | -------------------------------------------------------------------------------- /tests/fixtures/v5/metadata/esm/default1.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(import.meta.url.slice(7) + " ran") 5 | 6 | import {default as o} from 'ospec' 7 | o.localAssertions("override") 8 | 9 | 10 | o.spec(import.meta.url.slice(7), function() { 11 | 12 | o("test", function(o) { 13 | const md = o.metadata() 14 | console.log(md.file + " metadata file from test") 15 | console.log(md.name + " metadata name from test") 16 | o().satisfies(function() { 17 | const md = o.metadata() 18 | console.log(md.file + " metadata file from assertion") 19 | console.log(md.name + " metadata name from assertion") 20 | return {pass: true} 21 | }) 22 | }) 23 | 24 | }) -------------------------------------------------------------------------------- /tests/fixtures/v5/metadata/esm/default2.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(import.meta.url.slice(7) + " ran") 5 | 6 | import {default as o} from 'ospec' 7 | o.localAssertions("override") 8 | 9 | 10 | o.spec(import.meta.url.slice(7), function() { 11 | 12 | o("test", function(o) { 13 | const md = o.metadata() 14 | console.log(md.file + " metadata file from test") 15 | console.log(md.name + " metadata name from test") 16 | o().satisfies(function() { 17 | const md = o.metadata() 18 | console.log(md.file + " metadata file from assertion") 19 | console.log(md.name + " metadata name from assertion") 20 | return {pass: true} 21 | }) 22 | }) 23 | 24 | }) -------------------------------------------------------------------------------- /tests/fixtures/v5/metadata/esm/override.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict" 4 | console.log(import.meta.url.slice(7) + " ran") 5 | 6 | import {default as o} from 'ospec' 7 | o.localAssertions("override") 8 | 9 | 10 | o.metadata({file: "foo"}) 11 | 12 | o.spec(import.meta.url.slice(7), function() { 13 | 14 | o("test", function(o) { 15 | const md = o.metadata() 16 | console.log(md.file + " metadata file from test") 17 | console.log(md.name + " metadata name from test") 18 | o().satisfies(function() { 19 | const md = o.metadata() 20 | console.log(md.file + " metadata file from assertion") 21 | console.log(md.name + " metadata name from assertion") 22 | return {pass: true} 23 | }) 24 | }) 25 | 26 | }) -------------------------------------------------------------------------------- /tests/fixtures/v5/metadata/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "metadata": "ospec default1.js default2.js override.js", 5 | "which": "which ospec" 6 | }, 7 | "type": "module" 8 | } -------------------------------------------------------------------------------- /tests/fixtures/v5/success/cjs/explicit/explicit1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | o.localAssertions("override") 7 | 8 | o(__filename, (o) => { 9 | console.log(__filename + " had tests") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/cjs/explicit/explicit2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | o.localAssertions("override") 7 | 8 | o(__filename, (o) => { 9 | console.log(__filename + " had tests") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/cjs/main.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/cjs/other.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "default": "ospec", 5 | "explicit-one": "ospec ./explicit/explicit1.js", 6 | "explicit-several": "ospec ./explicit/explicit1.js ./explicit/explicit2.js", 7 | "explicit-glob": "ospec \"explicit/*.js\"", 8 | "ignore-one": "ospec --ignore tests/main2.js", 9 | "ignore-one-glob": "ospec --ignore \"very/**/*.js\"", 10 | "ignore-several": "ospec --ignore \"very/**\" --ignore tests/main2.js", 11 | "preload-one": "ospec --preload ./main.js", 12 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 13 | "require-one": "ospec --require ./main.js", 14 | "require-several": "ospec --require ./main.js --require ./other.js", 15 | "which": "which ospec" 16 | } 17 | } -------------------------------------------------------------------------------- /tests/fixtures/v5/success/cjs/tests/main1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | o.localAssertions("override") 7 | 8 | o(__filename, (o) => { 9 | console.log(__filename + " had tests") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/cjs/tests/main2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | o.localAssertions("override") 7 | 8 | o(__filename, (o) => { 9 | console.log(__filename + " had tests") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/cjs/very/deep/tests/deep1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | o.localAssertions("override") 7 | 8 | o(__filename, (o) => { 9 | console.log(__filename + " had tests") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/cjs/very/deep/tests/deeper/deep2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | o.localAssertions("override") 7 | 8 | o(__filename, (o) => { 9 | console.log(__filename + " had tests") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/config.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | exports["package.json"] = { 4 | "license": "ISC", 5 | "scripts": { 6 | "default": "ospec", 7 | "explicit-one": "ospec ./explicit/explicit1.js", 8 | "explicit-several": "ospec ./explicit/explicit1.js ./explicit/explicit2.js", 9 | "explicit-glob": "ospec \"explicit/*.js\"", 10 | // TODO investigate why --ignore is so capricious 11 | // `tests/test2.js` works, but `./tests/test2.js` doesn't. 12 | "ignore-one": "ospec --ignore tests/main2.js", 13 | "ignore-one-glob": "ospec --ignore \"very/**/*.js\"", 14 | "ignore-several": "ospec --ignore \"very/**\" --ignore tests/main2.js", 15 | "preload-one": "ospec --preload ./main.js", 16 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 17 | "require-one": "ospec --require ./main.js", 18 | "require-several": "ospec --require ./main.js --require ./other.js", 19 | "which" : "which ospec" 20 | } 21 | } 22 | 23 | const noTest = { 24 | cjs: ` 25 | "use strict" 26 | console.log(__filename + " ran") 27 | `, 28 | esm: ` 29 | "use strict" 30 | console.log(import.meta.url.slice(7) + " ran") 31 | 32 | `} 33 | 34 | const withTest = { 35 | cjs: ` 36 | "use strict" 37 | console.log(__filename + " ran") 38 | 39 | const o = require("ospec") 40 | o.localAssertions("override") 41 | 42 | o(__filename, (o) => { 43 | console.log(__filename + " had tests") 44 | o(true).equals(true) 45 | o(true).equals(true) 46 | }) 47 | `, 48 | esm: ` 49 | "use strict" 50 | console.log(import.meta.url.slice(7) + " ran") 51 | 52 | import {default as o} from 'ospec' 53 | o.localAssertions("override") 54 | 55 | o(import.meta.url.slice(7), (o) => { 56 | console.log(import.meta.url.slice(7) + " ran") 57 | o(true).equals(true) 58 | o(true).equals(true) 59 | }) 60 | `} 61 | 62 | exports["js"] = { 63 | explicit: { 64 | "explicit1.js": withTest, 65 | "explicit2.js": withTest, 66 | }, 67 | "main.js": noTest, 68 | "other.js": noTest, 69 | tests: { 70 | "main1.js": withTest, 71 | "main2.js": withTest, 72 | }, 73 | very: { 74 | deep: { 75 | "tests" : { 76 | "deep1.js": withTest, 77 | deeper: { 78 | "deep2.js": withTest 79 | } 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /tests/fixtures/v5/success/esm/explicit/explicit1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | o.localAssertions("override") 7 | 8 | o(import.meta.url.slice(7), (o) => { 9 | console.log(import.meta.url.slice(7) + " ran") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/esm/explicit/explicit2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | o.localAssertions("override") 7 | 8 | o(import.meta.url.slice(7), (o) => { 9 | console.log(import.meta.url.slice(7) + " ran") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/esm/main.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/esm/other.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "default": "ospec", 5 | "explicit-one": "ospec ./explicit/explicit1.js", 6 | "explicit-several": "ospec ./explicit/explicit1.js ./explicit/explicit2.js", 7 | "explicit-glob": "ospec \"explicit/*.js\"", 8 | "ignore-one": "ospec --ignore tests/main2.js", 9 | "ignore-one-glob": "ospec --ignore \"very/**/*.js\"", 10 | "ignore-several": "ospec --ignore \"very/**\" --ignore tests/main2.js", 11 | "preload-one": "ospec --preload ./main.js", 12 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 13 | "require-one": "ospec --require ./main.js", 14 | "require-several": "ospec --require ./main.js --require ./other.js", 15 | "which": "which ospec" 16 | }, 17 | "type": "module" 18 | } -------------------------------------------------------------------------------- /tests/fixtures/v5/success/esm/tests/main1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | o.localAssertions("override") 7 | 8 | o(import.meta.url.slice(7), (o) => { 9 | console.log(import.meta.url.slice(7) + " ran") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/esm/tests/main2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | o.localAssertions("override") 7 | 8 | o(import.meta.url.slice(7), (o) => { 9 | console.log(import.meta.url.slice(7) + " ran") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/esm/very/deep/tests/deep1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | o.localAssertions("override") 7 | 8 | o(import.meta.url.slice(7), (o) => { 9 | console.log(import.meta.url.slice(7) + " ran") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/success/esm/very/deep/tests/deeper/deep2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | o.localAssertions("override") 7 | 8 | o(import.meta.url.slice(7), (o) => { 9 | console.log(import.meta.url.slice(7) + " ran") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/cjs/main.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | throw __filename + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/cjs/other.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "default": "ospec", 5 | "preload-one": "ospec --preload ./main.js", 6 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 7 | "require-one": "ospec --require ./main.js", 8 | "require-several": "ospec --require ./main.js --require ./other.js", 9 | "which": "which ospec" 10 | } 11 | } -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/cjs/tests/main1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/cjs/tests/main2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | throw __filename + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/cjs/very/deep/tests/deep1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | throw __filename + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/cjs/very/deep/tests/deeper/deep2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(__filename + " ran") 4 | 5 | const o = require("ospec") 6 | o.localAssertions("override") 7 | 8 | o(__filename, (o) => { 9 | console.log(__filename + " had tests") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/config.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | exports["package.json"] = { 4 | "license": "ISC", 5 | "scripts": { 6 | "default": "ospec", 7 | "preload-one": "ospec --preload ./main.js", 8 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 9 | "require-one": "ospec --require ./main.js", 10 | "require-several": "ospec --require ./main.js --require ./other.js", 11 | "which" : "which ospec" 12 | } 13 | } 14 | 15 | const throws = { 16 | cjs: ` 17 | "use strict" 18 | console.log(__filename + " ran") 19 | throw __filename + " threw" 20 | `, 21 | esm: ` 22 | "use strict" 23 | console.log(import.meta.url.slice(7) + " ran") 24 | throw import.meta.url.slice(7) + " threw" 25 | `} 26 | 27 | const noThrowNoTest = { 28 | cjs: ` 29 | "use strict" 30 | console.log(__filename + " ran") 31 | `, 32 | esm: ` 33 | "use strict" 34 | console.log(import.meta.url.slice(7) + " ran") 35 | `} 36 | 37 | const noThrowWithTest = { 38 | cjs: ` 39 | "use strict" 40 | console.log(__filename + " ran") 41 | 42 | const o = require("ospec") 43 | o.localAssertions("override") 44 | 45 | o(__filename, (o) => { 46 | console.log(__filename + " had tests") 47 | o(true).equals(true) 48 | o(true).equals(true) 49 | }) 50 | `, 51 | esm: ` 52 | "use strict" 53 | console.log(import.meta.url.slice(7) + " ran") 54 | 55 | import {default as o} from 'ospec' 56 | o.localAssertions("override") 57 | 58 | o(import.meta.url.slice(7), (o) => { 59 | console.log(import.meta.url.slice(7) + " ran") 60 | o(true).equals(true) 61 | o(true).equals(true) 62 | }) 63 | `} 64 | 65 | exports["js"] = { 66 | "main.js": throws, 67 | "other.js": noThrowNoTest, 68 | tests: { 69 | "main1.js": noThrowNoTest, 70 | "main2.js": throws, 71 | }, 72 | very: { 73 | deep: { 74 | "tests" : { 75 | "deep1.js": throws, 76 | deeper: { 77 | "deep2.js": noThrowWithTest 78 | } 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/esm/main.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | throw import.meta.url.slice(7) + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/esm/other.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "default": "ospec", 5 | "preload-one": "ospec --preload ./main.js", 6 | "preload-several": "ospec --preload ./main.js --preload ./other.js", 7 | "require-one": "ospec --require ./main.js", 8 | "require-several": "ospec --require ./main.js --require ./other.js", 9 | "which": "which ospec" 10 | }, 11 | "type": "module" 12 | } -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/esm/tests/main1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/esm/tests/main2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | throw import.meta.url.slice(7) + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/esm/very/deep/tests/deep1.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | throw import.meta.url.slice(7) + " threw" 5 | -------------------------------------------------------------------------------- /tests/fixtures/v5/throws/esm/very/deep/tests/deeper/deep2.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict" 3 | console.log(import.meta.url.slice(7) + " ran") 4 | 5 | import {default as o} from 'ospec' 6 | o.localAssertions("override") 7 | 8 | o(import.meta.url.slice(7), (o) => { 9 | console.log(import.meta.url.slice(7) + " ran") 10 | o(true).equals(true) 11 | o(true).equals(true) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/test-cli.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable wrap-regex, no-process-env*/ 2 | "use strict" 3 | 4 | const loadFromDeps = ( 5 | typeof process !== "undefined" 6 | && process.argv.length >= 2 7 | && process.argv[1].match(/ospec-stable/) 8 | ) 9 | 10 | const isWindows = process.platform === "win32" 11 | 12 | const o = loadFromDeps ? require("ospec-stable") : require("../ospec") 13 | 14 | const {copyFile, lstat, mkdir, readdir, rmdir, symlink, unlink, writeFile} = require("node:fs/promises") 15 | const {join} = require("node:path") 16 | const {performance} = require("node:perf_hooks") 17 | const {spawnSync, spawn} = require("node:child_process") 18 | 19 | const linkOrShim = isWindows ? require("cmd-shim") : symlink 20 | 21 | const projectCwd = process.cwd() 22 | const ospecPkgJsonPath = join(projectCwd, "package.json") 23 | const ospecLibPath = join(projectCwd, "ospec.js") 24 | const ospecBinPath = join(projectCwd, "bin/ospec") 25 | const fixturesDir = (api) => join(projectCwd, `./tests/fixtures`, api) 26 | 27 | 28 | const parsed = /^v(\d+)\.(\d+)\./.exec(process.version) 29 | 30 | const supportsESM = parsed && Number(parsed[1]) > 13 || Number(parsed[1]) === 13 && Number(parsed[2]) >= 2 31 | 32 | const timeoutDelay = 20000 33 | 34 | const APIs = [ 35 | "legacy", 36 | "v5" 37 | ] 38 | 39 | const moduleKinds = supportsESM 40 | ? [ 41 | "cjs", 42 | "esm", 43 | ] 44 | : (console.log("Skipping ESM tests due to lack of platform support"), ["cjs"]) 45 | 46 | // ospec version is always "@current", regardless of `loadFromDeps`, which involves the runner in this 47 | // file, not the one being tested. 48 | 49 | const versions = {ospec:"@current"} 50 | const commands = [ 51 | "npm", 52 | "pnpm", 53 | "yarn" 54 | ].filter((launcher) => { 55 | try { 56 | const r = spawnSync(launcher, ["-v"], {shell: true}) 57 | versions[launcher] = ("@"+r.stdout).trim() 58 | return r.status === 0 59 | } catch(e) { 60 | return false 61 | } 62 | }) 63 | commands.unshift("ospec") 64 | 65 | console.log(`Testing (${ 66 | APIs.map(api => `${api} API`).join(" + ") 67 | }) x (${ 68 | moduleKinds.join(" + ") 69 | }) x (${ 70 | commands.map((c) => c+versions[c]).join(" + ") 71 | })`) 72 | 73 | function childPromise(child) { 74 | const err = [] 75 | const out = [] 76 | if (child.stdout) { 77 | child.stdout.on("data", (d) => out.push(d.toString())) 78 | child.stderr.on("data", (d) => err.push(d.toString())) 79 | } 80 | return Object.assign(new Promise(function (fulfill, _reject) { 81 | let code, signal 82 | const handler = (_code, _signal) => { 83 | code = _code, signal = _signal 84 | const result = { 85 | code, 86 | err: null, 87 | signal, 88 | stderr: err.join(""), 89 | stdout: out.join(""), 90 | } 91 | 92 | if (code === 0 && signal == null) fulfill(result) 93 | else { 94 | _reject(Object.assign(new Error("Problem in child process"), result)) 95 | } 96 | } 97 | 98 | child.on("close", handler) 99 | child.on("exit", handler) 100 | child.on("error", (error) => { 101 | _reject(Object.assign((error), { 102 | code, 103 | err: error, 104 | signal, 105 | stderr: err.join(""), 106 | stdout: out.join(""), 107 | })) 108 | if (child.exitCode == null) child.kill("SIGTERM") 109 | setTimeout(() => { 110 | if (child.exitCode == null) child.kill("SIGKILL") 111 | }, 200) 112 | }) 113 | }), {process: child}) 114 | } 115 | 116 | // This returns a Promise augmented with a `process` field for raw 117 | // access to the child process 118 | // The promise resolves to an object with this structure 119 | // { 120 | // code? // exit code, if any 121 | // signal? // signal recieved, if any 122 | // stdout: string, 123 | // stderr: string, 124 | // error?: the error caught, if any 125 | // } 126 | // On rejection, the Error is augmented with the same fields 127 | 128 | const readFromCmd = (cmd, options) => (...params) => childPromise(spawn(cmd, params.filter((p) => p !== ""), { 129 | env: process.env, 130 | cwd: process.cwd(), 131 | ...options 132 | })) 133 | 134 | // set PATH=%PATH%;.\node_modules\.bin 135 | // cmd /c "ospec.cmd foo.js" 136 | 137 | // $Env:PATH += ".\node_modules\.bin" 138 | // ospec foo.js 139 | 140 | function removeWarnings(stderr) { 141 | return stderr.split("\n").filter((x) => !x.includes("ExperimentalWarning") && !x.includes("npm WARN lifecycle")).join("\n") 142 | } 143 | function removeExtraOutputFor(command, stdout) { 144 | if (command === "yarn") return stdout.split("\n").filter((line) => !/^Done in [\d\.s]+$/.test(line) && !line.includes("yarnpkg")).join("\n") 145 | if (command === "pnpm") return stdout.replace( 146 | // eslint-disable-next-line no-irregular-whitespace 147 | /ERROR  Command failed with exit code \d+\./, 148 | "" 149 | ) 150 | return stdout 151 | } 152 | function checkIfFilesExist(cwd, files) { 153 | return Promise.all(files.map((list) => { 154 | const path = join(cwd, ...list.split("/")) 155 | return lstat(path).then( 156 | () => {o({found: true}).deepEquals({found: true})(path)} 157 | ).catch( 158 | (e) => o(e.stack).equals(false)("sanity check failed") 159 | ) 160 | })) 161 | } 162 | 163 | async function remove(path) { 164 | try { 165 | const stats = await lstat(path) 166 | if (stats.isDirectory()) { 167 | await Promise.all((await readdir(path)).map((child) => remove(join(path, child)))) 168 | return rmdir(path) 169 | } else { 170 | return unlink(path) 171 | } 172 | // eslint-disable-next-line no-empty 173 | } catch(e) { 174 | if (e.code !== "ENOENT") throw e 175 | } 176 | } 177 | 178 | async function createDir(js, prefix, mod) { 179 | await mkdir(prefix) 180 | for (const k in js) { 181 | const path = join(prefix, k) 182 | const content = js[k][mod] 183 | if (typeof content === "string") await writeFile(path, content) 184 | else await createDir(js[k], path, mod) 185 | } 186 | } 187 | async function createPackageJson(pkg, cwd, mod) { 188 | if (mod === "esm") pkg = {...pkg, type: "module"} 189 | await writeFile(join(cwd, "package.json"), JSON.stringify(pkg, null, "\t")) 190 | } 191 | async function createNodeModules(path) { 192 | const modulePath = join(path, "node_modules") 193 | const dotBinPath = join(modulePath, ".bin") 194 | 195 | const dummyCjsDir = join(modulePath, "dummy-module-with-tests-cjs") 196 | const dummyCjsTestDir = join(dummyCjsDir, "tests") 197 | 198 | const ospecLibDir = join(modulePath, "ospec") 199 | const ospecBinDir = join(ospecLibDir, "bin") 200 | 201 | await mkdir(dummyCjsTestDir, {recursive: true}) 202 | await writeFile( 203 | join(dummyCjsDir, "package.json"), 204 | '{"type": "commonjs"}' 205 | ) 206 | await writeFile( 207 | join(dummyCjsTestDir, "should-not-run.js"), 208 | "\"use strict\";console.log(__filename + ' ran')" 209 | ) 210 | 211 | await mkdir(ospecBinDir, {recursive: true}) 212 | await copyFile(ospecPkgJsonPath, join(ospecLibDir, "package.json")) 213 | await copyFile(ospecLibPath, join(ospecLibDir, "ospec.js")) 214 | await copyFile(ospecBinPath, join(ospecBinDir, "ospec")) 215 | 216 | await mkdir(dotBinPath) 217 | await linkOrShim(join(ospecBinDir, "ospec"), join(dotBinPath, "ospec")) 218 | } 219 | 220 | 221 | function expandPaths(o, result, prefix = "") { 222 | for (const k in o) { 223 | const path = join(prefix, k) 224 | if(typeof o[k] === "string") result.push(path) 225 | else expandPaths(o[k], result, path) 226 | } 227 | return result 228 | } 229 | 230 | const pathVarName = Object.keys(process.env).filter((k) => /^path$/i.test(k))[0] 231 | 232 | const env = { 233 | ...process.env, 234 | [pathVarName]: (isWindows ? ".\\node_modules\\.bin;": "./node_modules/.bin:") + process.env[pathVarName] 235 | } 236 | 237 | function runningIn({scenario, files}, suite) { 238 | // eslint-disable-next-line global-require 239 | APIs.forEach(api => { 240 | const scenarioPath = join(fixturesDir(api), scenario) 241 | const config = require(join(scenarioPath, "config.js")) 242 | const allFiles = expandPaths(config.js, ["node_modules/dummy-module-with-tests-cjs/tests/should-not-run.js"]) 243 | // `scenario` before `api` to avoid duplicate names in the parent spec 244 | o.spec(`${api} API > ${scenario}`, () => { 245 | moduleKinds.forEach((mod) => { 246 | o.spec(mod, () => { 247 | const cwd = join(scenarioPath, mod) 248 | o.before(async () => { 249 | o.timeout(timeoutDelay) 250 | await remove(cwd) 251 | await createDir(config.js, cwd, mod) 252 | await createPackageJson(config["package.json"], cwd, mod) 253 | await createNodeModules(cwd, mod) 254 | // sanity checks 255 | await checkIfFilesExist(cwd, files) 256 | const snrPath = join( 257 | cwd, "node_modules", "dummy-module-with-tests-cjs", "tests", "should-not-run.js" 258 | ) 259 | await readFromCmd("node", {cwd})(snrPath).then( 260 | ({code, stdout, stderr}) => { 261 | stdout = stdout.replace(/\r?\n$/, "") 262 | stderr = removeWarnings(stderr) 263 | o({code}).deepEquals({code: 0})(snrPath) 264 | o({stdout}).deepEquals({stdout: `${snrPath} ran`})(snrPath) 265 | o({stderr}).deepEquals({stderr: ""})(snrPath) 266 | }, 267 | ) 268 | }) 269 | commands.forEach((command) => { 270 | o.spec(command, () => { 271 | const {scripts} = config["package.json"] 272 | 273 | const run = ["npm", "pnpm", "yarn"].includes(command) 274 | ? readFromCmd(command, {cwd, shell: true}).bind(null, "run") 275 | : ( 276 | // slice off `ospec ` from the package.json::scripts entries 277 | (scenario) => readFromCmd(command, {cwd, env, shell: true})(scripts[scenario].slice(6)) 278 | ) 279 | 280 | let before 281 | o.before(() => { 282 | console.log(`[ ${scenario} + ${api} API + ${mod} + ${command}]`) 283 | before = performance.now() 284 | }) 285 | o.after(() => console.log(`...took ${Math.round(performance.now()-before)} ms`)) 286 | 287 | suite({allFiles, command, cwd, run: (arg) => run(arg)}) 288 | }) 289 | }) 290 | }) 291 | }) 292 | 293 | }) 294 | }) 295 | } 296 | 297 | function check({haystack, needle, label, expected}) { 298 | // const needle = join(cwd, file) + suffix 299 | const found = haystack.includes(needle) 300 | o({[label]: found}).deepEquals({[label]: expected})(haystack + "\n\nexpected: " + needle) 301 | } 302 | 303 | function checkWhoRanAndHAdTests({shouldRun, shouldTest, shouldThrow, cwd, stdout, stderr, allFiles}) { 304 | allFiles.forEach((file) => { 305 | const fullPath = join(cwd, file) 306 | check({ 307 | haystack: stdout, 308 | needle: fullPath + " ran", 309 | label: "ran", 310 | expected: shouldRun.has(file) 311 | }) 312 | check({ 313 | haystack: stdout, 314 | needle: fullPath + " had tests", 315 | label: "had tests", 316 | expected: shouldTest.has(file) 317 | }) 318 | if (shouldThrow != null) { 319 | check({ 320 | haystack: stderr, 321 | needle: fullPath, 322 | label: "threw", 323 | expected: shouldThrow.has(file) 324 | }) 325 | } 326 | }) 327 | } 328 | 329 | o.spec("cli", function() { 330 | runningIn({ 331 | scenario: "success", 332 | files: [ 333 | "explicit/explicit1.js", 334 | "explicit/explicit2.js", 335 | "main.js", 336 | "node_modules/dummy-module-with-tests-cjs/tests/should-not-run.js", 337 | "node_modules/ospec/bin/ospec", 338 | "node_modules/ospec/package.json", 339 | "node_modules/ospec/ospec.js", 340 | "other.js", 341 | "package.json", 342 | "tests/main1.js", 343 | "tests/main2.js", 344 | "very/deep/tests/deep1.js", 345 | "very/deep/tests/deeper/deep2.js" 346 | ] 347 | }, ({cwd, command, run, allFiles}) => { 348 | if (/^(?:npm|pnpm|yarn)$/.test(command)) o("which", async function() { 349 | o.timeout(timeoutDelay) 350 | let code, stdout, stderr 351 | try { 352 | void ({code, stdout, stderr} = await run("which")) 353 | } catch (e) { 354 | void ({code, stdout, stderr} = e) 355 | } 356 | stderr = removeWarnings(stderr) 357 | 358 | o({code}).deepEquals({code: 0}) 359 | o({stderr}).deepEquals({stderr: ""}) 360 | 361 | o({correctBinaryPath: stdout.includes(join(cwd, "node_modules/.bin/ospec"))}).deepEquals({correctBinaryPath: true})(stdout) 362 | }) 363 | o("default", async function() { 364 | o.timeout(timeoutDelay) 365 | let code, stdout, stderr 366 | try { 367 | void ({code, stdout, stderr} = await run("default")) 368 | } catch (e) { 369 | void ({code, stdout, stderr} = e) 370 | } 371 | stderr = removeWarnings(stderr) 372 | stdout = removeExtraOutputFor(command, stdout) 373 | 374 | o({code}).deepEquals({code: 0}) 375 | o({stderr}).deepEquals({stderr: ""}) 376 | 377 | o(/All 8 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 378 | 379 | const shouldRun = new Set([ 380 | "tests/main1.js", 381 | "tests/main2.js", 382 | "very/deep/tests/deep1.js", 383 | "very/deep/tests/deeper/deep2.js" 384 | ]) 385 | const shouldTest = new Set([ 386 | "tests/main1.js", 387 | "tests/main2.js", 388 | "very/deep/tests/deep1.js", 389 | "very/deep/tests/deeper/deep2.js" 390 | ]) 391 | 392 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 393 | }) 394 | o("explicit-one", async function() { 395 | o.timeout(timeoutDelay) 396 | let code, stdout, stderr 397 | try { 398 | void ({code, stdout, stderr} = await run("explicit-one")) 399 | } catch (e) { 400 | void ({code, stdout, stderr} = e) 401 | } 402 | stderr = removeWarnings(stderr) 403 | stdout = removeExtraOutputFor(command, stdout) 404 | 405 | o({code}).deepEquals({code: 0}) 406 | o({stderr}).deepEquals({stderr: ""}) 407 | 408 | o(/All 2 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 409 | 410 | const shouldRun = new Set([ 411 | "explicit/explicit1.js", 412 | ]) 413 | const shouldTest = new Set([ 414 | "explicit/explicit1.js", 415 | ]) 416 | 417 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 418 | }) 419 | o("explicit-several", async function() { 420 | o.timeout(timeoutDelay) 421 | let code, stdout, stderr 422 | try { 423 | void ({code, stdout, stderr} = await run("explicit-several")) 424 | } catch (e) { 425 | void ({code, stdout, stderr} = e) 426 | } 427 | stderr = removeWarnings(stderr) 428 | stdout = removeExtraOutputFor(command, stdout) 429 | 430 | o({code}).deepEquals({code: 0}) 431 | o({stderr}).deepEquals({stderr: ""}) 432 | 433 | o(/All 4 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 434 | 435 | const shouldRun = new Set([ 436 | "explicit/explicit1.js", 437 | "explicit/explicit2.js" 438 | ]) 439 | const shouldTest = new Set([ 440 | "explicit/explicit1.js", 441 | "explicit/explicit2.js" 442 | ]) 443 | 444 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 445 | }) 446 | o("explicit-glob", async function() { 447 | o.timeout(timeoutDelay) 448 | let code, stdout, stderr 449 | try { 450 | void ({code, stdout, stderr} = await run("explicit-glob")) 451 | } catch (e) { 452 | void ({code, stdout, stderr} = e) 453 | } 454 | stderr = removeWarnings(stderr) 455 | stdout = removeExtraOutputFor(command, stdout) 456 | 457 | o({code}).deepEquals({code: 0}) 458 | o({stderr}).deepEquals({stderr: ""}) 459 | 460 | o(/All 4 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 461 | 462 | const shouldRun = new Set([ 463 | "explicit/explicit1.js", 464 | "explicit/explicit2.js" 465 | ]) 466 | const shouldTest = new Set([ 467 | "explicit/explicit1.js", 468 | "explicit/explicit2.js" 469 | ]) 470 | 471 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 472 | }) 473 | o("preload-one", async function() { 474 | o.timeout(timeoutDelay) 475 | let code, stdout, stderr 476 | try { 477 | void ({code, stdout, stderr} = await run("preload-one")) 478 | } catch (e) { 479 | void ({code, stdout, stderr} = e) 480 | } 481 | stderr = removeWarnings(stderr) 482 | stdout = removeExtraOutputFor(command, stdout) 483 | 484 | o({code}).deepEquals({code: 0}) 485 | o({stderr}).deepEquals({stderr: ""}) 486 | 487 | o(/All 8 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 488 | 489 | const shouldRun = new Set([ 490 | "main.js", 491 | "tests/main1.js", 492 | "tests/main2.js", 493 | "very/deep/tests/deep1.js", 494 | "very/deep/tests/deeper/deep2.js" 495 | ]) 496 | const shouldTest = new Set([ 497 | "tests/main1.js", 498 | "tests/main2.js", 499 | "very/deep/tests/deep1.js", 500 | "very/deep/tests/deeper/deep2.js" 501 | ]) 502 | 503 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 504 | }) 505 | o("preload-several", async function() { 506 | o.timeout(timeoutDelay) 507 | let code, stdout, stderr 508 | try { 509 | void ({code, stdout, stderr} = await run("preload-several")) 510 | } catch (e) { 511 | void ({code, stdout, stderr} = e) 512 | } 513 | stderr = removeWarnings(stderr) 514 | stdout = removeExtraOutputFor(command, stdout) 515 | 516 | o({code}).deepEquals({code: 0}) 517 | o({stderr}).deepEquals({stderr: ""}) 518 | 519 | o(/All 8 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 520 | 521 | const shouldRun = new Set([ 522 | "main.js", 523 | "other.js", 524 | "tests/main1.js", 525 | "tests/main2.js", 526 | "very/deep/tests/deep1.js", 527 | "very/deep/tests/deeper/deep2.js" 528 | ]) 529 | const shouldTest = new Set([ 530 | "tests/main1.js", 531 | "tests/main2.js", 532 | "very/deep/tests/deep1.js", 533 | "very/deep/tests/deeper/deep2.js" 534 | ]) 535 | 536 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 537 | }) 538 | o("require-one", async function() { 539 | o.timeout(timeoutDelay) 540 | let code, stdout, stderr 541 | try { 542 | void ({code, stdout, stderr} = await run("require-one")) 543 | } catch (e) { 544 | void ({code, stdout, stderr} = e) 545 | } 546 | stderr = removeWarnings(stderr) 547 | stdout = removeExtraOutputFor(command, stdout) 548 | 549 | o({code}).deepEquals({code: 0}) 550 | o({stderr}).deepEquals({stderr: "Warning: The --require option has been deprecated, use --preload instead\n"}) 551 | 552 | o(/All 8 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 553 | 554 | const shouldRun = new Set([ 555 | "main.js", 556 | "tests/main1.js", 557 | "tests/main2.js", 558 | "very/deep/tests/deep1.js", 559 | "very/deep/tests/deeper/deep2.js" 560 | ]) 561 | const shouldTest = new Set([ 562 | "tests/main1.js", 563 | "tests/main2.js", 564 | "very/deep/tests/deep1.js", 565 | "very/deep/tests/deeper/deep2.js" 566 | ]) 567 | 568 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 569 | }) 570 | o("require-several", async function() { 571 | o.timeout(timeoutDelay) 572 | let code, stdout, stderr 573 | try { 574 | void ({code, stdout, stderr} = await run("require-several")) 575 | } catch (e) { 576 | void ({code, stdout, stderr} = e) 577 | } 578 | stderr = removeWarnings(stderr) 579 | stdout = removeExtraOutputFor(command, stdout) 580 | 581 | o({code}).deepEquals({code: 0}) 582 | o({stderr}).deepEquals({stderr: "Warning: The --require option has been deprecated, use --preload instead\n"}) 583 | 584 | o(/All 8 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 585 | 586 | const shouldRun = new Set([ 587 | "main.js", 588 | "other.js", 589 | "tests/main1.js", 590 | "tests/main2.js", 591 | "very/deep/tests/deep1.js", 592 | "very/deep/tests/deeper/deep2.js" 593 | ]) 594 | const shouldTest = new Set([ 595 | "tests/main1.js", 596 | "tests/main2.js", 597 | "very/deep/tests/deep1.js", 598 | "very/deep/tests/deeper/deep2.js" 599 | ]) 600 | 601 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 602 | }) 603 | o("ignore-one", async function() { 604 | o.timeout(timeoutDelay) 605 | let code, stdout, stderr 606 | try { 607 | void ({code, stdout, stderr} = await run("ignore-one")) 608 | } catch (e) { 609 | void ({code, stdout, stderr} = e) 610 | } 611 | stderr = removeWarnings(stderr) 612 | stdout = removeExtraOutputFor(command, stdout) 613 | 614 | o({code}).deepEquals({code: 0}) 615 | o({stderr}).deepEquals({stderr: ""}) 616 | 617 | o(/All 6 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 618 | 619 | const shouldRun = new Set([ 620 | "tests/main1.js", 621 | "very/deep/tests/deep1.js", 622 | "very/deep/tests/deeper/deep2.js" 623 | ]) 624 | const shouldTest = new Set([ 625 | "tests/main1.js", 626 | "very/deep/tests/deep1.js", 627 | "very/deep/tests/deeper/deep2.js" 628 | ]) 629 | 630 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 631 | }) 632 | o("ignore-one-glob", async function() { 633 | o.timeout(timeoutDelay) 634 | let code, stdout, stderr 635 | try { 636 | void ({code, stdout, stderr} = await run("ignore-one-glob")) 637 | } catch (e) { 638 | void ({code, stdout, stderr} = e) 639 | } 640 | stderr = removeWarnings(stderr) 641 | stdout = removeExtraOutputFor(command, stdout) 642 | 643 | o({code}).deepEquals({code: 0}) 644 | o({stderr}).deepEquals({stderr: ""}) 645 | 646 | o(/All 4 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 647 | 648 | const shouldRun = new Set([ 649 | "tests/main1.js", 650 | "tests/main2.js", 651 | ]) 652 | const shouldTest = new Set([ 653 | "tests/main1.js", 654 | "tests/main2.js", 655 | ]) 656 | 657 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 658 | }) 659 | o("ignore-several", async function() { 660 | o.timeout(timeoutDelay) 661 | let code, stdout, stderr 662 | try { 663 | void ({code, stdout, stderr} = await run("ignore-several")) 664 | } catch (e) { 665 | void ({code, stdout, stderr} = e) 666 | } 667 | stderr = removeWarnings(stderr) 668 | stdout = removeExtraOutputFor(command, stdout) 669 | 670 | o({code}).deepEquals({code: 0}) 671 | o({stderr}).deepEquals({stderr: ""}) 672 | 673 | o(/All 2 assertions passed/.test(stdout)).equals(true)(stdout.match(/\n[^\n]+\n$/)) 674 | 675 | const shouldRun = new Set([ 676 | "tests/main1.js", 677 | ]) 678 | const shouldTest = new Set([ 679 | "tests/main1.js", 680 | ]) 681 | 682 | checkWhoRanAndHAdTests({shouldRun, shouldTest, cwd, stdout, stderr, allFiles}) 683 | }) 684 | }) 685 | 686 | /////////// 687 | 688 | /////////// 689 | 690 | /////////// 691 | 692 | /////////// 693 | 694 | runningIn({ 695 | scenario: "throws", 696 | files: [ 697 | "main.js", 698 | "node_modules/dummy-module-with-tests-cjs/tests/should-not-run.js", 699 | "node_modules/ospec/bin/ospec", 700 | "node_modules/ospec/package.json", 701 | "node_modules/ospec/ospec.js", 702 | "other.js", 703 | "package.json", 704 | "tests/main1.js", 705 | "tests/main2.js", 706 | "very/deep/tests/deep1.js", 707 | "very/deep/tests/deeper/deep2.js" 708 | ] 709 | }, ({cwd, command, run, allFiles}) => { 710 | if (/^(?:npm|pnpm|yarn)$/.test(command)) o("which", async function() { 711 | o.timeout(timeoutDelay) 712 | let code, stdout, stderr 713 | try { 714 | void ({code, stdout, stderr} = await run("which")) 715 | } catch (e) { 716 | void ({code, stdout, stderr} = e) 717 | } 718 | stderr = removeWarnings(stderr) 719 | 720 | o({code}).deepEquals({code: 0}) 721 | o({stderr}).deepEquals({stderr: ""}) 722 | 723 | o({correctBinaryPath: stdout.includes(join(cwd, "node_modules/.bin/ospec"))}).deepEquals({correctBinaryPath: true})(stdout) 724 | }) 725 | o("default", async function() { 726 | o.timeout(timeoutDelay) 727 | let code, stdout, stderr 728 | try { 729 | void ({code, stdout, stderr} = await run("default")) 730 | } catch (e) { 731 | void ({code, stdout, stderr} = e) 732 | } 733 | 734 | stderr = removeWarnings(stderr) 735 | stdout = removeExtraOutputFor(command, stdout) 736 | 737 | o({code}).deepEquals({code: 1}) 738 | o({stderr}).notDeepEquals({stderr: ""}) 739 | 740 | o({correctNumberPassed: /All 2 assertions passed\. Bailed out 2 times\s+(ELIFECYCLE[^]*)?$/.test(stdout)}) 741 | .deepEquals({correctNumberPassed: true})(stdout.match(/\n[^\n]+\n[^\n]+\n$/)) 742 | 743 | const shouldRun = new Set([ 744 | "tests/main1.js", 745 | "tests/main2.js", 746 | "very/deep/tests/deep1.js", 747 | "very/deep/tests/deeper/deep2.js" 748 | ]) 749 | const shouldTest = new Set([ 750 | "tests/main1.js", 751 | "tests/main2.js", 752 | "very/deep/tests/deep1.js", 753 | "very/deep/tests/deeper/deep2.js" 754 | ]) 755 | 756 | const shouldThrow = new Set([ 757 | "tests/main2.js", 758 | "very/deep/tests/deep1.js" 759 | ]) 760 | 761 | checkWhoRanAndHAdTests({shouldRun, shouldTest, shouldThrow, cwd, stdout, stderr, allFiles}) 762 | }) 763 | 764 | o("preload-one", async function() { 765 | o.timeout(timeoutDelay) 766 | let code, stdout, stderr 767 | try { 768 | void ({code, stdout, stderr} = await run("preload-one")) 769 | } catch (e) { 770 | void ({code, stdout, stderr} = e) 771 | } 772 | 773 | stderr = removeWarnings(stderr) 774 | stdout = removeExtraOutputFor(command, stdout) 775 | 776 | o({code}).deepEquals({code: 1}) 777 | o({"could not preload": stderr.includes("could not preload ./main.js")}).deepEquals({"could not preload": true}) 778 | 779 | o({assertionReport: /\d+ assertions (?:pass|fail)ed/.test(stdout)}) 780 | .deepEquals({assertionReport: false})(stdout) 781 | 782 | const shouldRun = new Set([ 783 | "main.js", 784 | ]) 785 | const shouldTest = new Set([ 786 | ]) 787 | const shouldThrow = new Set([ 788 | "main.js" 789 | ]) 790 | 791 | checkWhoRanAndHAdTests({shouldRun, shouldTest, shouldThrow, cwd, stdout, stderr, allFiles}) 792 | 793 | }) 794 | o("preload-several", async function() { 795 | o.timeout(timeoutDelay) 796 | let code, stdout, stderr; 797 | try { 798 | void ({code, stdout, stderr} = await run("preload-several")) 799 | } catch (e) { 800 | void ({code, stdout, stderr} = e) 801 | } 802 | 803 | stderr = removeWarnings(stderr) 804 | stdout = removeExtraOutputFor(command, stdout) 805 | 806 | o({code}).deepEquals({code: 1}) 807 | o({"could not preload": stderr.includes("could not preload ./main.js")}).deepEquals({"could not preload": true}) 808 | 809 | o({assertionReport: /\d+ assertions (?:pass|fail)ed/.test(stdout)}) 810 | .deepEquals({assertionReport: false})(stdout) 811 | 812 | const shouldRun = new Set([ 813 | "main.js", 814 | ]) 815 | const shouldTest = new Set([ 816 | ]) 817 | const shouldThrow = new Set([ 818 | "main.js" 819 | ]) 820 | 821 | checkWhoRanAndHAdTests({shouldRun, shouldTest, shouldThrow, cwd, stdout, stderr, allFiles}) 822 | }) 823 | }) 824 | runningIn({ 825 | scenario: "metadata", 826 | files: [ 827 | "node_modules/dummy-module-with-tests-cjs/tests/should-not-run.js", 828 | "node_modules/ospec/bin/ospec", 829 | "node_modules/ospec/package.json", 830 | "node_modules/ospec/ospec.js", 831 | "package.json", 832 | "default1.js", 833 | "default2.js", 834 | "override.js", 835 | ] 836 | }, ({cwd, command, run}) => { 837 | if (/^(?:npm|pnpm|yarn)$/.test(command)) o("which", async function() { 838 | o.timeout(timeoutDelay) 839 | let code, stdout, stderr 840 | try { 841 | void ({code, stdout, stderr} = await run("which")) 842 | } catch (e) { 843 | void ({code, stdout, stderr} = e) 844 | } 845 | stderr = removeWarnings(stderr) 846 | 847 | o({code}).deepEquals({code: 0}) 848 | o({stderr}).deepEquals({stderr: ""}) 849 | 850 | o({correctBinaryPath: stdout.includes(join(cwd, "node_modules/.bin/ospec"))}).deepEquals({correctBinaryPath: true})(stdout) 851 | }) 852 | o("metadata", async function() { 853 | o.timeout(timeoutDelay) 854 | let code, stdout, stderr 855 | try { 856 | void ({code, stdout, stderr} = await run("metadata")) 857 | } catch (e) { 858 | void ({code, stdout, stderr} = e) 859 | } 860 | stderr = removeWarnings(stderr) 861 | stdout = removeExtraOutputFor(command, stdout) 862 | 863 | o({code}).deepEquals({code: 0}) 864 | o({stderr}).deepEquals({stderr: ""}) 865 | 866 | o({correctNumberPassed: /All 3 assertions passed/.test(stdout)}) 867 | .deepEquals({correctNumberPassed: true})(stdout.match(/\n[^\n]+\n[^\n]+\n$/)) 868 | const files = [ 869 | "default1.js", "default2.js", "override.js" 870 | ] 871 | files.forEach((file) => { 872 | const fullPath = join(cwd, file) 873 | const metadataFile = file === "override.js" ? "foo" : fullPath 874 | check({ 875 | haystack: stdout, 876 | needle: fullPath + " ran", 877 | label: "ran", 878 | expected: true 879 | }) 880 | 881 | check({ 882 | haystack: stdout, 883 | // __filename is also the name of the spec 884 | needle: fullPath + " > test metadata name from test", 885 | label: "metadata name from test", 886 | expected: true 887 | }) 888 | 889 | check({ 890 | haystack: stdout, 891 | needle: metadataFile + " metadata file from test", 892 | label: "metadata file from test", 893 | expected: true 894 | }) 895 | 896 | check({ 897 | haystack: stdout, 898 | needle: fullPath + " > test metadata name from assertion", 899 | label: "metadata name from assertion", 900 | expected: true 901 | }) 902 | 903 | check({ 904 | haystack: stdout, 905 | needle: metadataFile + " metadata file from assertion", 906 | label: "metadata file from assertion", 907 | expected: true 908 | }) 909 | }) 910 | }) 911 | }) 912 | }) 913 | -------------------------------------------------------------------------------- /tests/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------