├── .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 | [](https://www.npmjs.com/package/ospec) [](https://www.npmjs.com/package/ospec)  [](https://www.npmjs.com/package/ospec)
4 |
5 | [](https://opencollective.com/mithriljs) [](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 | [](https://www.npmjs.com/package/ospec) [](https://www.npmjs.com/package/ospec)  [](https://www.npmjs.com/package/ospec)
4 |
5 | [](https://opencollective.com/mithriljs) [](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 |
--------------------------------------------------------------------------------