├── perfs ├── .gitignore ├── scripts │ ├── test-files-cpu.js │ └── test-files-io.js ├── package.json └── readme.md ├── pta ├── test │ └── samples │ │ ├── es │ │ ├── src │ │ │ └── sum.js │ │ ├── package.json │ │ └── test │ │ │ ├── pos-integer.mjs │ │ │ └── neg-integer.js │ │ ├── cjs │ │ ├── src │ │ │ └── sum.js │ │ ├── package.json │ │ └── test │ │ │ ├── neg-integer.js │ │ │ └── pos-integer.cjs │ │ ├── serial │ │ ├── shared-state.js │ │ ├── a.spec.js │ │ ├── c.spec.js │ │ └── b.spec.js │ │ ├── dummy │ │ ├── test.js │ │ ├── tap.txt │ │ └── log.txt │ │ ├── failing │ │ └── test.js │ │ ├── errored │ │ └── test.js │ │ ├── only │ │ └── spec.js │ │ └── index.js ├── src │ ├── usage.txt │ └── bin.js ├── readme.md └── package.json ├── assert ├── test │ ├── index.js │ ├── factory.js │ └── assert.js ├── package.json ├── src │ ├── index.js │ ├── utils.js │ ├── index.d.ts │ └── assert.js └── readme.md ├── media ├── diff.png └── tap.png ├── examples ├── node-ts │ ├── test │ │ ├── index.ts │ │ └── specs │ │ │ ├── is-odd.ts │ │ │ └── is-even.ts │ ├── src │ │ ├── is-even.ts │ │ ├── index.ts │ │ └── is-odd.ts │ ├── tsconfig.json │ └── package.json ├── node-es │ ├── src │ │ ├── is-even.js │ │ ├── is-odd.js │ │ └── index.js │ ├── test │ │ ├── index.js │ │ └── specs │ │ │ ├── is-odd.js │ │ │ └── is-even.js │ └── package.json ├── browser-vanilla-vite │ ├── .gitignore │ ├── test │ │ ├── index.html │ │ ├── util.js │ │ ├── specs │ │ │ └── is-even-component.js │ │ └── runner.js │ ├── example │ │ └── index.html │ ├── package.json │ ├── src │ │ └── is-even-component.js │ ├── bin.js │ └── package-lock.json └── readme.md ├── reporters ├── src │ ├── diff │ │ ├── diagnostic │ │ │ ├── timeout.js │ │ │ ├── is.js │ │ │ ├── isNot.js │ │ │ ├── notEqual.js │ │ │ ├── notOk.js │ │ │ ├── ok.js │ │ │ ├── fail.js │ │ │ ├── throws.js │ │ │ ├── index.js │ │ │ ├── equal.test.js │ │ │ └── equal.js │ │ ├── stack.js │ │ ├── utils.js │ │ ├── writer │ │ │ ├── diagnostic.js │ │ │ ├── summary.js │ │ │ ├── index.js │ │ │ ├── summary.test.js │ │ │ └── diagnostic.test.js │ │ ├── theme.js │ │ ├── utils.test.js │ │ └── index.js │ ├── index.js │ ├── usage.txt │ ├── log │ │ └── index.js │ ├── message-parser.js │ ├── protocol.js │ ├── counter.js │ ├── utils.test.js │ ├── bin.js │ ├── types │ │ └── index.d.ts │ ├── tap │ │ ├── index.js │ │ ├── writer.js │ │ └── writer.test.js │ ├── utils.js │ ├── counter.test.js │ └── message-parser.test.js ├── rollup.js ├── rollup-types.js ├── package.json └── readme.md ├── zora ├── test │ ├── samples │ │ ├── bailout.txt │ │ ├── flush.txt │ │ ├── bailout_nested.txt │ │ ├── only.txt │ │ ├── failing.js │ │ ├── late_collect.js │ │ ├── async.txt │ │ ├── simple.txt │ │ ├── only.js │ │ ├── no_only_mode.js │ │ ├── skip.txt │ │ ├── simple.js │ │ ├── failing_nested.js │ │ ├── circular_ref_obj.js │ │ ├── flush.js │ │ ├── failing.txt │ │ ├── skip.js │ │ ├── bailout.js │ │ ├── failing_nested.txt │ │ ├── custom_assertion.txt │ │ ├── symbol.js │ │ ├── circular_ref_obj.txt │ │ ├── only_nested.txt │ │ ├── custom_assertion.js │ │ ├── bailout_nested.js │ │ ├── async.js │ │ ├── nested.txt │ │ ├── nested_async.txt │ │ ├── symbol.txt │ │ ├── only_nested.js │ │ ├── no_only_mode_nested.js │ │ ├── nested.js │ │ ├── timeout.js │ │ ├── nested_async.js │ │ └── timeout.txt │ └── run.js ├── rollup-types.js ├── src │ ├── env.js │ ├── harness.js │ ├── index.js │ ├── types │ │ └── index.d.ts │ └── test.js ├── rollup.js ├── package.json └── readme.md ├── .github └── workflows │ └── test.yml ├── .gitignore ├── package.json ├── LICENSE ├── contributing.md └── readme.md /perfs/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /perfs/scripts/test-files-cpu.js: -------------------------------------------------------------------------------- 1 | // todo 2 | -------------------------------------------------------------------------------- /pta/test/samples/es/src/sum.js: -------------------------------------------------------------------------------- 1 | export default (a, b) => a + b; 2 | -------------------------------------------------------------------------------- /assert/test/index.js: -------------------------------------------------------------------------------- 1 | import './factory.js'; 2 | import './assert.js'; 3 | -------------------------------------------------------------------------------- /pta/test/samples/cjs/src/sum.js: -------------------------------------------------------------------------------- 1 | module.exports = (a, b) => a + b; 2 | -------------------------------------------------------------------------------- /pta/test/samples/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cjs-sample-test" 3 | } 4 | -------------------------------------------------------------------------------- /media/diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenzofox3/zora/master/media/diff.png -------------------------------------------------------------------------------- /media/tap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenzofox3/zora/master/media/tap.png -------------------------------------------------------------------------------- /examples/node-ts/test/index.ts: -------------------------------------------------------------------------------- 1 | import './specs/is-odd'; 2 | import './specs/is-even'; 3 | -------------------------------------------------------------------------------- /pta/test/samples/serial/shared-state.js: -------------------------------------------------------------------------------- 1 | export const counter = { 2 | value: 0, 3 | }; 4 | -------------------------------------------------------------------------------- /examples/node-es/src/is-even.js: -------------------------------------------------------------------------------- 1 | import isEven from 'is-even'; 2 | 3 | export default isEven; 4 | -------------------------------------------------------------------------------- /examples/node-es/test/index.js: -------------------------------------------------------------------------------- 1 | import './specs/is-odd.js'; 2 | import './specs/is-even.js'; 3 | -------------------------------------------------------------------------------- /examples/browser-vanilla-vite/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /pta/test/samples/es/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "es-sample-test", 3 | "type": "module" 4 | } 5 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/timeout.js: -------------------------------------------------------------------------------- 1 | export default () => 2 | ({ actual }) => 3 | actual; 4 | -------------------------------------------------------------------------------- /examples/node-es/src/is-odd.js: -------------------------------------------------------------------------------- 1 | import isEven from 'is-even'; 2 | 3 | export default (input) => !isEven(input); 4 | -------------------------------------------------------------------------------- /examples/node-ts/src/is-even.ts: -------------------------------------------------------------------------------- 1 | import isEven from 'is-even'; 2 | 3 | export default isEven as (input: number) => boolean; 4 | -------------------------------------------------------------------------------- /zora/test/samples/bailout.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # will not go to the end 3 | ok 1 - okay 4 | Bail out! Unhandled error. 5 | -------------------------------------------------------------------------------- /examples/node-ts/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as isEven } from './is-even'; 2 | export { default as isOdd } from './is-odd'; 3 | -------------------------------------------------------------------------------- /examples/node-ts/src/is-odd.ts: -------------------------------------------------------------------------------- 1 | import isEven from 'is-even'; 2 | 3 | export default (input: number): boolean => !isEven(input); 4 | -------------------------------------------------------------------------------- /examples/node-es/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as isEven } from './is-even.js'; 2 | export { default as isOdd } from './is-odd.js'; 3 | -------------------------------------------------------------------------------- /pta/test/samples/dummy/test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | 3 | test('sample', ({ ok }) => { 4 | ok(true, 'woot woot'); 5 | }); 6 | -------------------------------------------------------------------------------- /pta/test/samples/failing/test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | 3 | test('will fail', (t) => { 4 | t.fail(`some failure`); 5 | }); 6 | -------------------------------------------------------------------------------- /pta/test/samples/errored/test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | 3 | test('will throw an error', (t) => { 4 | throw new Error('some error'); 5 | }); 6 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/is.js: -------------------------------------------------------------------------------- 1 | export default (theme) => () => 2 | `expected ${theme.emphasis('references to be the same')} but they were not`; 3 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/isNot.js: -------------------------------------------------------------------------------- 1 | export default (theme) => () => 2 | `expected ${theme.emphasis('references not to be the same')} but they were`; 3 | -------------------------------------------------------------------------------- /pta/test/samples/dummy/tap.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # sample 3 | ok 1 - woot woot 4 | 5 | 1..1 6 | # tests 1 7 | # pass 1 8 | # fail 0 9 | # skip 0 10 | -------------------------------------------------------------------------------- /zora/test/samples/flush.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # tester flush 3 | ok 1 - assert1 4 | 5 | 1..1 6 | # tests 1 7 | # pass 1 8 | # fail 0 9 | # skip 0 10 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/notEqual.js: -------------------------------------------------------------------------------- 1 | export default (theme) => () => 2 | `expected the arguments ${theme.emphasis( 3 | 'not to be equivalent' 4 | )} but they were`; 5 | -------------------------------------------------------------------------------- /zora/test/samples/bailout_nested.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # will not go to the end 3 | ok 1 - okay 4 | # some sub tester 5 | ok 2 - before the end ... 6 | Bail out! Unhandled error. 7 | -------------------------------------------------------------------------------- /zora/test/samples/only.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | ok 1 - should not run # SKIP 3 | # should run 4 | ok 2 - I ran 5 | 6 | 1..2 7 | # tests 2 8 | # pass 1 9 | # fail 0 10 | # skip 1 11 | -------------------------------------------------------------------------------- /zora/test/samples/failing.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | test('tester 1', (t) => { 4 | t.ok(true, 'assert1'); 5 | t.equal('foo', 'bar', 'foo should equal bar'); 6 | }); 7 | -------------------------------------------------------------------------------- /pta/test/samples/es/test/pos-integer.mjs: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import sum from '../src/sum.js'; 3 | 4 | test('valid sum', (t) => { 5 | t.eq(sum(2, 1), 3); 6 | t.eq(sum(0, 42), 42); 7 | }); 8 | -------------------------------------------------------------------------------- /examples/node-ts/test/specs/is-odd.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { isOdd } from '../../src/index'; 3 | 4 | test(`isOdd`, (t) => { 5 | t.eq(isOdd(1), true); 6 | t.eq(isOdd(2), true); 7 | }); 8 | -------------------------------------------------------------------------------- /pta/test/samples/es/test/neg-integer.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import sum from '../src/sum.js'; 3 | 4 | test('valid sum', (t) => { 5 | t.eq(sum(2, -1), 1); 6 | t.eq(sum(0, -42), -42); 7 | }); 8 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/notOk.js: -------------------------------------------------------------------------------- 1 | export default (theme) => 2 | ({ actual }) => 3 | `expected ${theme.emphasis('"falsy"')} but got ${theme.emphasis( 4 | JSON.stringify(actual) 5 | )}`; 6 | -------------------------------------------------------------------------------- /examples/node-es/test/specs/is-odd.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { isOdd } from '../../src/index.js'; 3 | 4 | test(`isOdd`, (t) => { 5 | t.eq(isOdd(1), true); 6 | t.eq(isOdd(2), true); 7 | }); 8 | -------------------------------------------------------------------------------- /examples/node-ts/test/specs/is-even.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { isEven } from '../../src/index'; 3 | 4 | test(`isEven`, (t) => { 5 | t.eq(isEven(1), true); 6 | t.eq(isEven(2), true); 7 | }); 8 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/ok.js: -------------------------------------------------------------------------------- 1 | export default (theme) => 2 | ({ actual }) => 3 | `expected ${theme.emphasis('"truthy"')} but got ${theme.emphasis( 4 | actual === '' ? '""' : actual 5 | )}`; 6 | -------------------------------------------------------------------------------- /pta/test/samples/cjs/test/neg-integer.js: -------------------------------------------------------------------------------- 1 | const { test } = require('zora'); 2 | const sum = require('../src/sum.js'); 3 | 4 | test('valid sum', (t) => { 5 | t.eq(sum(2, -1), 1); 6 | t.eq(sum(0, -42), -42); 7 | }); 8 | -------------------------------------------------------------------------------- /pta/test/samples/cjs/test/pos-integer.cjs: -------------------------------------------------------------------------------- 1 | const { test } = require('zora'); 2 | const sum = require('../src/sum.js'); 3 | 4 | test('valid sum', (t) => { 5 | t.eq(sum(2, 1), 3); 6 | t.eq(sum(0, 42), 42); 7 | }); 8 | -------------------------------------------------------------------------------- /zora/test/samples/late_collect.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | test(`late collection`, async (t) => { 4 | t.ok(true); 5 | 6 | setTimeout(() => { 7 | t.ok(true); 8 | }, 50); 9 | }); 10 | -------------------------------------------------------------------------------- /zora/test/samples/async.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # tester 1 3 | ok 1 - assert1 4 | ok 2 - assert2 5 | # tester 2 6 | ok 3 - assert3 7 | ok 4 - assert4 8 | 9 | 1..4 10 | # tests 4 11 | # pass 4 12 | # fail 0 13 | # skip 0 14 | -------------------------------------------------------------------------------- /zora/test/samples/simple.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # tester 1 3 | ok 1 - assert1 4 | ok 2 - assert2 5 | # tester 2 6 | ok 3 - assert3 7 | ok 4 - assert4 8 | 9 | 1..4 10 | # tests 4 11 | # pass 4 12 | # fail 0 13 | # skip 0 14 | -------------------------------------------------------------------------------- /zora/test/samples/only.js: -------------------------------------------------------------------------------- 1 | import { only, test } from '../../src/index.js'; 2 | 3 | test('should not run', (t) => { 4 | t.fail('I should not run '); 5 | }); 6 | 7 | only('should run', (t) => { 8 | t.ok(true, 'I ran'); 9 | }); 10 | -------------------------------------------------------------------------------- /zora/test/samples/no_only_mode.js: -------------------------------------------------------------------------------- 1 | import { test, only } from '../../src/index.js'; 2 | 3 | test('should not run', (t) => { 4 | t.fail('I should not run '); 5 | }); 6 | 7 | only('should run', (t) => { 8 | t.ok(true, 'I ran'); 9 | }); 10 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/fail.js: -------------------------------------------------------------------------------- 1 | export default (theme) => 2 | ({ description }) => 3 | `expected ${theme.emphasis( 4 | 'fail' 5 | )} not to be called, but was called as ${theme.emphasis( 6 | JSON.stringify(description) 7 | )}`; 8 | -------------------------------------------------------------------------------- /reporters/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as createTAPReporter } from './tap/index.js'; 2 | export { default as createJSONReporter } from './log/index.js'; 3 | export { default as createDiffReporter } from './diff/index.js'; 4 | export * from './protocol.js'; 5 | -------------------------------------------------------------------------------- /zora/test/samples/skip.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # hello world 3 | ok 1 - should be truthy 4 | ok 2 - blah # SKIP 5 | ok 3 - for some reason # SKIP 6 | ok 4 - failing text # SKIP 7 | 8 | 1..4 9 | # tests 4 10 | # pass 1 11 | # fail 0 12 | # skip 3 13 | -------------------------------------------------------------------------------- /reporters/rollup.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: './src/index.js', 3 | output: [ 4 | { 5 | format: 'cjs', 6 | file: './dist/index.cjs', 7 | }, 8 | { 9 | format: 'es', 10 | file: './dist/index.js' 11 | } 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /zora/test/samples/simple.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | test('tester 1', (t) => { 4 | t.ok(true, 'assert1'); 5 | t.ok(true, 'assert2'); 6 | }); 7 | 8 | test('tester 2', (t) => { 9 | t.ok(true, 'assert3'); 10 | t.ok(true, 'assert4'); 11 | }); 12 | -------------------------------------------------------------------------------- /zora/test/samples/failing_nested.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | test('tester 1', (t) => { 4 | t.ok(true, 'assert1'); 5 | t.test('inside', (t) => { 6 | t.ok(true, 'correct'); 7 | t.equal(4, '4', 'should fail with coercion'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/browser-vanilla-vite/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /zora/test/samples/circular_ref_obj.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | test('circular ref in diagnostic', (t) => { 4 | const b = { key: 'prop' }; 5 | const a = { foo: 'bar', b }; 6 | 7 | b.a = a; 8 | 9 | t.eq(a, { foo: 'bar', b: { key: 'prop' } }); 10 | }); 11 | -------------------------------------------------------------------------------- /pta/test/samples/dummy/log.txt: -------------------------------------------------------------------------------- 1 | {"type":"TEST_START","data":{"description":"sample","skip":false}} 2 | {"type":"ASSERTION","data":{"pass":true,"actual":true,"expected":"truthy value","description":"woot woot","operator":"ok"}} 3 | {"type":"TEST_END","data":{"description":"sample","executionTime":{TIME}}} 4 | -------------------------------------------------------------------------------- /zora/test/samples/flush.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | const wait = (time) => 4 | new Promise((resolve) => { 5 | setTimeout(() => resolve(), time); 6 | }); 7 | 8 | test('tester flush', async (t) => { 9 | await wait(1000); 10 | t.ok(true, 'assert1'); 11 | }); 12 | -------------------------------------------------------------------------------- /examples/node-es/test/specs/is-even.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { isEven } from '../../src/index.js'; 3 | 4 | test(`isEven`, (t) => { 5 | t.eq(isEven(1), true); 6 | t.eq(isEven(2), true); 7 | t.eq({ foo: 'bar', prop: 2 }, { foo: 'baz', prop: 2 }); 8 | t.eq('foo', 'for'); 9 | }); 10 | -------------------------------------------------------------------------------- /zora/rollup-types.js: -------------------------------------------------------------------------------- 1 | import dts from 'rollup-plugin-dts'; 2 | 3 | export default { 4 | input: 'src/types/index.d.ts', 5 | output: [{ 6 | file: 'dist/index.d.ts', 7 | format: 'es', 8 | },{ 9 | file: 'dist/index.d.cts', 10 | format: 'cjs', 11 | }], 12 | plugins: [dts()], 13 | }; 14 | -------------------------------------------------------------------------------- /zora/test/samples/failing.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # tester 1 3 | ok 1 - assert1 4 | not ok 2 - foo should equal bar 5 | --- 6 | actual: "foo" 7 | expected: "bar" 8 | operator: "equal" 9 | at:{STACK} 10 | ... 11 | 12 | 1..2 13 | # tests 2 14 | # pass 1 15 | # fail 1 16 | # skip 0 17 | -------------------------------------------------------------------------------- /zora/test/samples/skip.js: -------------------------------------------------------------------------------- 1 | import { test, skip } from '../../src/index.js'; 2 | 3 | test('hello world', (t) => { 4 | t.ok(true); 5 | t.skip('blah', (t) => { 6 | t.ok(false); 7 | }); 8 | t.skip('for some reason'); 9 | }); 10 | 11 | skip('failing text', (t) => { 12 | t.ok(false); 13 | }); 14 | -------------------------------------------------------------------------------- /pta/test/samples/serial/a.spec.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { setTimeout } from 'node:timers/promises'; 3 | import { counter } from './shared-state.js'; 4 | 5 | await test(`first one`, async (t) => { 6 | await setTimeout(300); 7 | counter.value += 1; 8 | t.eq(counter.value, 1); 9 | }); 10 | -------------------------------------------------------------------------------- /pta/test/samples/serial/c.spec.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { setTimeout } from 'node:timers/promises'; 3 | import { counter } from './shared-state.js'; 4 | 5 | await test(`a third one`, async (t) => { 6 | await setTimeout(100); 7 | counter.value += 1; 8 | t.eq(counter.value, 3); 9 | }); 10 | -------------------------------------------------------------------------------- /reporters/rollup-types.js: -------------------------------------------------------------------------------- 1 | import dts from 'rollup-plugin-dts'; 2 | 3 | export default { 4 | input: 'src/types/index.d.ts', 5 | output: [{ 6 | file: 'dist/index.d.ts', 7 | format: 'es', 8 | }, { 9 | file: 'dist/index.d.cts', 10 | format: 'cjs' 11 | }], 12 | plugins: [dts()], 13 | }; 14 | -------------------------------------------------------------------------------- /pta/test/samples/serial/b.spec.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { setTimeout } from 'node:timers/promises'; 3 | import { counter } from './shared-state.js'; 4 | 5 | await test(`a second one`, async (t) => { 6 | await setTimeout(200); 7 | counter.value += 1; 8 | t.eq(counter.value, 2); 9 | }); 10 | -------------------------------------------------------------------------------- /pta/test/samples/only/spec.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | 3 | test.only('should run', ({ only, test }) => { 4 | test('should not run', ({ ok }) => { 5 | ok(false); 6 | }); 7 | 8 | only('only', (t) => { 9 | t.ok(true); 10 | }); 11 | }); 12 | 13 | test('failing', ({ ok }) => { 14 | ok(false); 15 | }); 16 | -------------------------------------------------------------------------------- /zora/test/samples/bailout.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | test('will not go to the end', (t) => { 4 | t.ok(true, 'okay'); 5 | 6 | throw new Error('Oh no!'); 7 | 8 | t.ok(true, 'will never be reached'); 9 | }); 10 | 11 | test('another one', (t) => { 12 | t.ok(true, 'will never be reported'); 13 | }); 14 | -------------------------------------------------------------------------------- /zora/test/samples/failing_nested.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # tester 1 3 | ok 1 - assert1 4 | # inside 5 | ok 2 - correct 6 | not ok 3 - should fail with coercion 7 | --- 8 | actual: 4 9 | expected: "4" 10 | operator: "equal" 11 | at:{STACK} 12 | ... 13 | 14 | 1..3 15 | # tests 3 16 | # pass 2 17 | # fail 1 18 | # skip 0 19 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/throws.js: -------------------------------------------------------------------------------- 1 | export default (theme) => (m) => { 2 | const { actual, expected } = m; 3 | return expected !== void 0 4 | ? `expected the error thrown to match ${theme.emphasis( 5 | expected 6 | )} but it matched ${theme.emphasis(actual)}` 7 | : `expected ${theme.emphasis('to throw')} but it did not`; 8 | }; 9 | -------------------------------------------------------------------------------- /zora/test/samples/custom_assertion.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # tester 1 3 | ok 1 - foo should equal foo 4 | not ok 2 - should be "foo" 5 | --- 6 | expected: "foo" 7 | actual: "blah" 8 | operator: "isFoo" 9 | other: "property" 10 | at:{STACK} 11 | ... 12 | 13 | 1..2 14 | # tests 2 15 | # pass 1 16 | # fail 1 17 | # skip 0 18 | -------------------------------------------------------------------------------- /zora/test/samples/symbol.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | test('symbol tester 1', (t) => { 4 | t.equal(Symbol('foo'), Symbol('bar'), 'Symbol foo should equal Symbol bar'); 5 | t.equal( 6 | { symbol: Symbol('foo') }, 7 | { symbol: Symbol('bar') }, 8 | 'Property Symbol foo should equal Symbol bar' 9 | ); 10 | }); 11 | -------------------------------------------------------------------------------- /reporters/src/diff/stack.js: -------------------------------------------------------------------------------- 1 | export const createStack = () => { 2 | const items = []; 3 | const stack = { 4 | [Symbol.iterator]() { 5 | return items[Symbol.iterator](); 6 | }, 7 | push(item) { 8 | items.push(item); 9 | return stack; 10 | }, 11 | pop() { 12 | return items.pop(); 13 | }, 14 | }; 15 | return stack; 16 | }; 17 | -------------------------------------------------------------------------------- /examples/browser-vanilla-vite/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /zora/test/samples/circular_ref_obj.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # circular ref in diagnostic 3 | not ok 1 - should be equivalent 4 | --- 5 | actual: {"foo":"bar","b":{"key":"prop","a":"[__CIRCULAR_REF__]"}} 6 | expected: {"foo":"bar","b":{"key":"prop"}} 7 | operator: "equal" 8 | at:{STACK} 9 | ... 10 | 11 | 1..1 12 | # tests 1 13 | # pass 0 14 | # fail 1 15 | # skip 0 16 | -------------------------------------------------------------------------------- /zora/test/samples/only_nested.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | ok 1 - should not run # SKIP 3 | # should run 4 | ok 2 - I ran 5 | # keep running 6 | # keeeeeep running 7 | ok 3 - I got there 8 | ok 4 - should not run # SKIP 9 | # should run but nothing inside 10 | ok 5 - will not run # SKIP 11 | ok 6 - will not run # SKIP 12 | 13 | 1..6 14 | # tests 6 15 | # pass 2 16 | # fail 0 17 | # skip 4 18 | -------------------------------------------------------------------------------- /examples/browser-vanilla-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "node bin.js" 6 | }, 7 | "devDependencies": { 8 | "@lorenzofox3/for-await": "^0.2.2", 9 | "playwright": "^1.22.2", 10 | "vite": "^2.9.18", 11 | "ws": "^8.8.0", 12 | "zora": "*" 13 | }, 14 | "dependencies": { 15 | "is-even": "~1.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /zora/test/samples/custom_assertion.js: -------------------------------------------------------------------------------- 1 | import { Assert, test } from '../../src/index.js'; 2 | 3 | Assert.isFoo = (value, description = 'should be "foo"') => ({ 4 | pass: value === 'foo', 5 | expected: 'foo', 6 | actual: value, 7 | operator: 'isFoo', 8 | description, 9 | other: 'property' 10 | }); 11 | 12 | test('tester 1', (t) => { 13 | t.equal('foo', 'foo', 'foo should equal foo'); 14 | t.isFoo('blah'); 15 | }); 16 | -------------------------------------------------------------------------------- /zora/src/env.js: -------------------------------------------------------------------------------- 1 | export const findConfigurationValue = (name) => { 2 | if (isNode) { 3 | return process.env[name]; 4 | } else if (isDeno) { 5 | return Deno.env.get(name); 6 | } else if (isBrowser) { 7 | return window[name]; 8 | } 9 | }; 10 | 11 | export const isNode = typeof process !== 'undefined'; 12 | export const isBrowser = typeof window !== 'undefined'; 13 | export const isDeno = typeof Deno !== 'undefined'; 14 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | # Recipes list 2 | 3 | A list of example projects with various dev environments: 4 | 5 | * [simple node library written as an ecmascript module](./node-es) 6 | * [simple node library written in typescript](./node-ts) 7 | 8 | You should usually have a look at the ``scripts`` section of the package.json files to find out how to run the tests, the dev mode, the coverage, etc. There are often alternative scripts and examples as well 9 | -------------------------------------------------------------------------------- /zora/test/samples/bailout_nested.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | test('will not go to the end', (t) => { 4 | t.ok(true, 'okay'); 5 | 6 | t.test('some sub tester', async (t) => { 7 | t.ok(true, 'before the end ...'); 8 | throw new Error('Oh no!'); 9 | }); 10 | 11 | t.ok(true, 'will never be reached'); 12 | }); 13 | 14 | test('another one', (t) => { 15 | t.ok(true, 'will never be reported'); 16 | }); 17 | -------------------------------------------------------------------------------- /zora/test/samples/async.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | const wait = (time) => 4 | new Promise((resolve) => { 5 | setTimeout(() => resolve(), time); 6 | }); 7 | 8 | test('tester 1', async (t) => { 9 | t.ok(true, 'assert1'); 10 | await wait(500); 11 | t.ok(true, 'assert2'); 12 | }); 13 | 14 | test('tester 2', async (t) => { 15 | t.ok(true, 'assert3'); 16 | await wait(300); 17 | t.ok(true, 'assert4'); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/node-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "incremental": true, 8 | "sourceMap": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "lib": [ 13 | "ES2020" 14 | ] 15 | }, 16 | "exclude": [ 17 | "node_modules/" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /reporters/src/usage.txt: -------------------------------------------------------------------------------- 1 | 2 | Synopsis 3 | Transforms a zora testing protocol message stream into a specific reporting format 4 | 5 | Usage 6 | zr 7 | 8 | Options 9 | --reporter, -R One of "tap", "diff"(default). 10 | --strict-mode If true, throws an error when the stream parser finds a message it cannot understand (default false) 11 | 12 | Examples 13 | zr -R tap < some/file.txt 14 | ZORA_REPORTER=json node zora/program.js | zr -R diff 15 | 16 | -------------------------------------------------------------------------------- /zora/test/samples/nested.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # tester 1 3 | ok 1 - assert1 4 | # some nested tester 5 | ok 2 - nested 1 6 | ok 3 - nested 2 7 | # some nested tester bis 8 | ok 4 - nested 1 9 | # deeply nested 10 | ok 5 - deeply nested really 11 | ok 6 - deeply nested again 12 | ok 7 - nested 2 13 | ok 8 - assert2 14 | # tester 2 15 | ok 9 - assert3 16 | # nested in two 17 | ok 10 - still happy 18 | ok 11 - assert4 19 | 20 | 1..11 21 | # tests 11 22 | # pass 11 23 | # fail 0 24 | # skip 0 25 | -------------------------------------------------------------------------------- /reporters/src/log/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | compose, 3 | defaultLogger, 4 | defaultSerializer, 5 | eventuallySetExitCode, 6 | } from '../utils.js'; 7 | 8 | export default ({ 9 | log = defaultLogger, 10 | serialize = defaultSerializer, 11 | } = {}) => { 12 | const print = compose([log, serialize]); 13 | return async (messageStream) => { 14 | for await (const message of messageStream) { 15 | eventuallySetExitCode(message); 16 | print(message); 17 | } 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /zora/test/samples/nested_async.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # tester 1 3 | # update counter with delay 4 | ok 1 - nested 1 5 | ok 2 - nested 2 6 | # check counter 7 | ok 3 - should see the old value of the counter 8 | ok 4 - assert2 9 | # tester 2 10 | ok 5 - assert3 11 | # update counter with delay but blocking 12 | ok 6 - nested 1 13 | ok 7 - nested 2 14 | # check counter bis 15 | ok 8 - should see the new value of the counter 16 | ok 9 - whatever 17 | 18 | 1..9 19 | # tests 9 20 | # pass 9 21 | # fail 0 22 | # skip 0 23 | -------------------------------------------------------------------------------- /zora/rollup.js: -------------------------------------------------------------------------------- 1 | import node from '@rollup/plugin-node-resolve'; 2 | import cjs from '@rollup/plugin-commonjs'; 3 | 4 | export default { 5 | input: './src/index.js', 6 | output: [ 7 | { 8 | file: './dist/index.cjs', 9 | format: 'cjs', 10 | }, 11 | { 12 | file: './dist/index.js', 13 | format: 'es', 14 | }, 15 | ], 16 | treeshake: { 17 | moduleSideEffects: false, // otherwise package 'diff' from 'zora-reporters' gets included 18 | }, 19 | plugins: [node(), cjs()], 20 | }; 21 | -------------------------------------------------------------------------------- /reporters/src/message-parser.js: -------------------------------------------------------------------------------- 1 | import { unknownMessage } from './protocol.js'; 2 | 3 | export const createJSONParser = ({ strictMode = false }) => 4 | async function* (lineStream) { 5 | for await (const line of lineStream) { 6 | try { 7 | yield JSON.parse(line); 8 | } catch (e) { 9 | if (strictMode) { 10 | throw new Error(`could not parse line "${line}"`); 11 | } else { 12 | yield unknownMessage({ message: line }); 13 | } 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /zora/test/samples/symbol.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # symbol tester 1 3 | not ok 1 - Symbol foo should equal Symbol bar 4 | --- 5 | actual: "Symbol(foo)" 6 | expected: "Symbol(bar)" 7 | operator: "equal" 8 | at:{STACK} 9 | ... 10 | not ok 2 - Property Symbol foo should equal Symbol bar 11 | --- 12 | actual: {"symbol":"Symbol(foo)"} 13 | expected: {"symbol":"Symbol(bar)"} 14 | operator: "equal" 15 | at:{STACK} 16 | ... 17 | 18 | 1..2 19 | # tests 2 20 | # pass 0 21 | # fail 2 22 | # skip 0 23 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/index.js: -------------------------------------------------------------------------------- 1 | export { default as okDiagnostic } from './ok.js'; 2 | export { default as notOkDiagnostic } from './notOk.js'; 3 | export { default as failDiagnostic } from './fail.js'; 4 | export { default as notEqualDiagnostic } from './notEqual.js'; 5 | export { default as isDiagnostic } from './is.js'; 6 | export { default as isNotDiagnostic } from './isNot.js'; 7 | export { default as throwsDiagnostic } from './throws.js'; 8 | export { default as equalDiagnostic } from './equal.js'; 9 | export { default as timeoutDiagnostic } from './timeout.js'; 10 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | jobs: 16 | all: 17 | name: build and test 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: 20 24 | - name: Install 25 | run: npm run install:ci 26 | - name: Build 27 | run: npm run build 28 | - name: Test 29 | run: npm t 30 | 31 | -------------------------------------------------------------------------------- /examples/browser-vanilla-vite/test/util.js: -------------------------------------------------------------------------------- 1 | export const shallowMount = (comp) => { 2 | const wrapper = document.createElement('div'); 3 | if (typeof comp === 'string') { 4 | wrapper.innerHTML = comp; 5 | } else { 6 | wrapper.appendChild(comp); 7 | } 8 | if (comp.connectedCallback) { 9 | comp.connectedCallback(); 10 | } 11 | return wrapper.firstChild; 12 | }; 13 | 14 | export const map = (mapFn) => 15 | async function* (stream) { 16 | for await (const element of stream) { 17 | yield mapFn(element); 18 | } 19 | }; 20 | 21 | export const compose = (fns) => (arg) => fns.reduceRight((y, fn) => fn(y), arg); 22 | -------------------------------------------------------------------------------- /pta/src/usage.txt: -------------------------------------------------------------------------------- 1 | 2 | Synopsis 3 | Test runner for zora spec files 4 | 5 | Usage 6 | pta [ ...] 7 | 8 | Options 9 | --only Runs zora in "only mode" 10 | 11 | --reporter, -R One of tap, log. Otherwise it will use the default reporter 12 | 13 | --timeout change the default test timeout, in milliseconde. Default is 5000ms 14 | 15 | Examples 16 | pta 17 | pta test/{unit,int}/**/*.js 18 | 19 | If no argument is provided, the CLI will use the following patterns: 20 | - **/test.js 21 | - **/*.spec.js 22 | - **/*.test.js 23 | - **/test/**/*.js 24 | - **/tests/**/*.js 25 | - **/__tests__/**/*.js 26 | 27 | -------------------------------------------------------------------------------- /reporters/src/diff/utils.js: -------------------------------------------------------------------------------- 1 | import { compose } from '../utils.js'; 2 | 3 | const curry2 = (fn) => (a, b) => { 4 | if (b === void 0) { 5 | return (b) => fn(a, b); 6 | } 7 | 8 | return fn(a, b); 9 | }; 10 | 11 | export const leftPad = curry2((offset, string) => 12 | string.padStart(string.length + offset) 13 | ); 14 | 15 | export const rightPad = curry2((offset, string) => 16 | string.padEnd(string.length + offset) 17 | ); 18 | 19 | export const withMargin = compose([leftPad(1), rightPad(1)]); 20 | 21 | export const typeAsString = (value) => { 22 | if (typeof value === 'object') { 23 | return value?.constructor?.name ?? String(value); 24 | } 25 | return typeof value; 26 | }; 27 | -------------------------------------------------------------------------------- /zora/test/samples/only_nested.js: -------------------------------------------------------------------------------- 1 | import { only, test } from '../../src/index.js'; 2 | 3 | test('should not run', (t) => { 4 | t.fail('I should not run '); 5 | }); 6 | 7 | only('should run', (t) => { 8 | t.ok(true, 'I ran'); 9 | 10 | t.only('keep running', (t) => { 11 | t.only('keeeeeep running', (t) => { 12 | t.ok(true, ' I got there'); 13 | }); 14 | }); 15 | 16 | t.test('should not run', (t) => { 17 | t.fail('shouldn ot run'); 18 | }); 19 | }); 20 | 21 | only('should run but nothing inside', (t) => { 22 | t.test('will not run', (t) => { 23 | t.fail('should not run'); 24 | }); 25 | t.test('will not run', (t) => { 26 | t.fail('should not run'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /zora/test/samples/no_only_mode_nested.js: -------------------------------------------------------------------------------- 1 | import { test, only } from '../../src/index.js'; 2 | 3 | test('should not run', (t) => { 4 | t.fail('I should not run '); 5 | }); 6 | 7 | only('should run', (t) => { 8 | t.ok(true, 'I ran'); 9 | 10 | t.only('keep running', (t) => { 11 | t.only('keeeeeep running', (t) => { 12 | t.ok(true, ' I got there'); 13 | }); 14 | }); 15 | 16 | t.test('should not run', (t) => { 17 | t.fail('shouldn ot run'); 18 | }); 19 | }); 20 | 21 | only('should run but nothing inside', (t) => { 22 | t.test('will not run', (t) => { 23 | t.fail('should not run'); 24 | }); 25 | t.test('will not run', (t) => { 26 | t.fail('should not run'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /assert/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zora-assert", 3 | "version": "0.0.1", 4 | "description": "assertion library for zora", 5 | "type": "module", 6 | "types": "./src/index.d.ts", 7 | "exports": { 8 | "./package.json": "./package.json", 9 | ".": { 10 | "import": "./src/index.js", 11 | "types": "./src/index.d.ts" 12 | } 13 | }, 14 | "scripts": { 15 | "test": "node ./test/index.js", 16 | "test:watch": "node --watch ./test/index.js", 17 | "dev": "npm run test:watch" 18 | }, 19 | "author": "@lorenzofox3 ", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "fast-deep-equal": "~3.1.3" 23 | }, 24 | "prettier": { 25 | "singleQuote": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /assert/src/index.js: -------------------------------------------------------------------------------- 1 | import { decorateWithLocation } from './utils.js'; 2 | import { Assert as AssertPrototype } from './assert.js'; 3 | 4 | const noop = () => {}; 5 | 6 | const hook = (onResult) => (assert) => 7 | Object.fromEntries( 8 | Object.keys(AssertPrototype).map((methodName) => [ 9 | methodName, 10 | (...args) => onResult(assert[methodName](...args)), 11 | ]) 12 | ); 13 | 14 | export default ( 15 | { onResult = noop } = { 16 | onResult: noop, 17 | } 18 | ) => { 19 | const hookOnAssert = hook((item) => { 20 | const result = decorateWithLocation(item); 21 | onResult(result); 22 | return result; 23 | }); 24 | 25 | return hookOnAssert(Object.create(AssertPrototype)); 26 | }; 27 | 28 | export const Assert = AssertPrototype; 29 | -------------------------------------------------------------------------------- /zora/test/samples/nested.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | test('tester 1', (t) => { 4 | t.ok(true, 'assert1'); 5 | 6 | t.test('some nested tester', (t) => { 7 | t.ok(true, 'nested 1'); 8 | t.ok(true, 'nested 2'); 9 | }); 10 | 11 | t.test('some nested tester bis', (t) => { 12 | t.ok(true, 'nested 1'); 13 | 14 | t.test('deeply nested', (t) => { 15 | t.ok(true, 'deeply nested really'); 16 | t.ok(true, 'deeply nested again'); 17 | }); 18 | 19 | t.ok(true, 'nested 2'); 20 | }); 21 | 22 | t.ok(true, 'assert2'); 23 | }); 24 | 25 | test('tester 2', (t) => { 26 | t.ok(true, 'assert3'); 27 | 28 | t.test('nested in two', (t) => { 29 | t.ok(true, 'still happy'); 30 | }); 31 | 32 | t.ok(true, 'assert4'); 33 | }); 34 | -------------------------------------------------------------------------------- /examples/browser-vanilla-vite/test/specs/is-even-component.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import IsEvenComponent from '../../src/is-even-component.js'; 3 | import { shallowMount } from '../util.js'; 4 | 5 | test('is-even component', ({ test }) => { 6 | test('isEven getter should be true at first', (t) => { 7 | const comp = shallowMount(document.createElement('is-even')); 8 | t.eq(comp.isEven, true); 9 | t.eq(comp.value, 42, 'default value should be 42'); 10 | }); 11 | 12 | test('isEven getter should be true when the value is even, false otherwise', (t) => { 13 | const comp = shallowMount(document.createElement('is-even')); 14 | t.eq(comp.isEven, true); 15 | t.eq(comp.value, 2); 16 | comp.value = 1; 17 | t.eq(comp.isEven, false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc __tests__ coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .idea 40 | 41 | dist 42 | suites 43 | 44 | .DS_Store 45 | -------------------------------------------------------------------------------- /pta/readme.md: -------------------------------------------------------------------------------- 1 | # PTA 2 | 3 | Test runner for any zora testing program, using node (version >= 16). 4 | 5 | ## Installation 6 | 7 | ``npm i -g pta`` 8 | 9 | ## Usage 10 | 11 | example: 12 | 13 | ``pta test/**/*.js`` 14 | 15 | Note: it should work with both module format commonjs and ES 16 | 17 | ## Options 18 | 19 | ``` 20 | Usage 21 | pta [ ...] 22 | 23 | Options 24 | --only Runs zora in "only mode" 25 | 26 | --reporter, -R One of tap, log. Otherwise it will use the default reporter 27 | 28 | 29 | Examples 30 | pta 31 | pta test/{unit,int}/**/*.js 32 | 33 | If no argument is provided, the CLI will use the following patterns: 34 | - **/test.js 35 | - **/*.spec.js 36 | - **/*.test.js 37 | - **/test/**/*.js 38 | - **/tests/**/*.js 39 | - **/__tests__/**/*.js 40 | ``` 41 | 42 | -------------------------------------------------------------------------------- /reporters/src/diff/writer/diagnostic.js: -------------------------------------------------------------------------------- 1 | import { 2 | okDiagnostic, 3 | notOkDiagnostic, 4 | failDiagnostic, 5 | notEqualDiagnostic, 6 | isDiagnostic, 7 | isNotDiagnostic, 8 | throwsDiagnostic, 9 | equalDiagnostic, 10 | timeoutDiagnostic, 11 | } from '../diagnostic/index.js'; 12 | 13 | export default ({ theme }) => { 14 | const operators = { 15 | ok: okDiagnostic(theme), 16 | notOk: notOkDiagnostic(theme), 17 | fail: failDiagnostic(theme), 18 | notEqual: notEqualDiagnostic(theme), 19 | is: isDiagnostic(theme), 20 | isNot: isNotDiagnostic(theme), 21 | throws: throwsDiagnostic(theme), 22 | equal: equalDiagnostic(theme), 23 | timeout: timeoutDiagnostic(theme), 24 | }; 25 | 26 | const unknown = ({ operator }) => 27 | `unknown operator ${theme.emphasis(operator)}`; 28 | 29 | return (diag) => operators[diag.operator]?.(diag) ?? unknown(diag); 30 | }; 31 | -------------------------------------------------------------------------------- /pta/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pta", 3 | "version": "1.3.0", 4 | "description": "Test runner for nodejs using zora", 5 | "type": "module", 6 | "bin": { 7 | "pta": "src/bin.js" 8 | }, 9 | "exports": { 10 | "./package.json": "./package.json" 11 | }, 12 | "scripts": { 13 | "test": "node test/samples/index.js" 14 | }, 15 | "author": "@lorenzofox3 ", 16 | "license": "MIT", 17 | "prettier": { 18 | "singleQuote": true 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/lorenzofox3/zora.git" 23 | }, 24 | "files": [ 25 | "src" 26 | ], 27 | "keywords": [ 28 | "zora", 29 | "test", 30 | "testing", 31 | "cli", 32 | "test-runner" 33 | ], 34 | "dependencies": { 35 | "arg": "^5.0.2", 36 | "globby": "^14.0.0", 37 | "zora-reporters": "*" 38 | }, 39 | "peerDependencies": { 40 | "zora": "^6" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /reporters/src/protocol.js: -------------------------------------------------------------------------------- 1 | export const MESSAGE_TYPE = { 2 | TEST_START: 'TEST_START', 3 | ASSERTION: 'ASSERTION', 4 | TEST_END: 'TEST_END', 5 | ERROR: 'ERROR', 6 | UNKNOWN: 'UNKNOWN', 7 | }; 8 | 9 | export const newTestMessage = ({ description, skip }) => ({ 10 | type: MESSAGE_TYPE.TEST_START, 11 | data: { description, skip }, 12 | }); 13 | 14 | export const assertionMessage = (data) => ({ 15 | type: MESSAGE_TYPE.ASSERTION, 16 | data, 17 | }); 18 | 19 | export const testEndMessage = ({ description, executionTime }) => ({ 20 | type: MESSAGE_TYPE.TEST_END, 21 | data: { 22 | description, 23 | executionTime, 24 | }, 25 | }); 26 | 27 | export const errorMessage = ({ error }) => ({ 28 | type: MESSAGE_TYPE.ERROR, 29 | data: { 30 | error, 31 | }, 32 | }); 33 | 34 | export const unknownMessage = ({ message }) => ({ 35 | type: MESSAGE_TYPE.UNKNOWN, 36 | data: { 37 | message, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /examples/node-es/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "es", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "node test/index.js", 9 | "dev": "nodemon test/index.js", 10 | "test:only": "ZORA_ONLY=true npm t", 11 | "test:coverage": "c8 node test/index.js", 12 | "test:pta-as-runner": "pta ./test/specs/", 13 | "test:pretty-reporter": "ZORA_REPORTER=json node test/index.js | zr", 14 | "test:json-pipeline": "ZORA_REPORTER=json node test/index.js | grep '\"pass\":false' | fx .data", 15 | "test:tap-pipeline": "node test/index.js | tap-difflet" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "is-even": "~1.0.0" 21 | }, 22 | "devDependencies": { 23 | "c8": "^8.0.1", 24 | "fx": "^31.0.0", 25 | "nodemon": "^3.0.2", 26 | "pta": "*", 27 | "tap-difflet": "~0.7.2", 28 | "zora": "*" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /zora/src/harness.js: -------------------------------------------------------------------------------- 1 | import { Assert, createAssert } from './test.js'; 2 | 3 | export const createHarness = ({ onlyMode = false } = {}) => { 4 | const tests = []; 5 | 6 | // WARNING if the "onlyMode is passed to any harness, all the harnesses will be affected. 7 | // However, we do not expect multiple harnesses to be used in the same process 8 | if (onlyMode) { 9 | const { skip, test } = Assert; 10 | Assert.test = skip; 11 | Assert.only = test; 12 | } 13 | 14 | const { test, skip, only } = createAssert({ 15 | onResult: (test) => tests.push(test), 16 | }); 17 | 18 | // for convenience 19 | test.only = only; 20 | test.skip = skip; 21 | 22 | return { 23 | only, 24 | test, 25 | skip, 26 | report({ reporter }) { 27 | return reporter(createMessageStream(tests)); 28 | }, 29 | }; 30 | }; 31 | 32 | async function* createMessageStream(tests) { 33 | for (const test of tests) { 34 | yield* test; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /zora/test/samples/timeout.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | import { setTimeout } from 'node:timers/promises'; 3 | 4 | test( 5 | 'broken promise', 6 | ({ ok }) => { 7 | return new Promise(() => { 8 | }).then(() => { 9 | ok(true); 10 | }); 11 | }, 12 | { timeout: 500 } 13 | ); 14 | 15 | test('timeout in a nested test', ({ test }) => { 16 | test( 17 | 'nested', 18 | ({ ok }) => { 19 | return new Promise(() => { 20 | }).then(() => { 21 | ok(true); 22 | }); 23 | }, 24 | { timeout: 500 } 25 | ); 26 | }); 27 | 28 | test('just too long', ({ test }) => { 29 | test( 30 | 'on time', 31 | async ({ ok }) => { 32 | await setTimeout(700); 33 | ok(true); 34 | }, 35 | { timeout: 1000 } 36 | ); 37 | 38 | test( 39 | 'too late', 40 | async ({ ok }) => { 41 | await setTimeout(700); 42 | ok(true); 43 | }, 44 | { timeout: 500 } 45 | ); 46 | }); 47 | -------------------------------------------------------------------------------- /examples/browser-vanilla-vite/src/is-even-component.js: -------------------------------------------------------------------------------- 1 | import isEven from 'is-even'; 2 | 3 | const template = document.createElement('template'); 4 | 5 | template.innerHTML = `
6 | 7 |
`; 8 | 9 | class IsEvenComponent extends HTMLElement { 10 | get value() { 11 | const inputValue = this.shadowRoot.getElementById('input').value; 12 | return inputValue !== void 0 ? Number(inputValue) : 0; 13 | } 14 | 15 | set value(val) { 16 | this.shadowRoot.getElementById('input').value = Number(val); 17 | } 18 | 19 | get isEven() { 20 | return isEven(this.value) === true; 21 | } 22 | 23 | constructor() { 24 | super(); 25 | this.attachShadow({ mode: 'open' }); 26 | this.shadowRoot.appendChild(template.content.cloneNode(true)); 27 | } 28 | 29 | connectedCallback() { 30 | this.value = 2; 31 | } 32 | } 33 | 34 | customElements.define('is-even', IsEvenComponent); 35 | 36 | export default IsEvenComponent; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zora", 3 | "workspaces": [ 4 | "assert", 5 | "reporters", 6 | "zora", 7 | "pta" 8 | ], 9 | "engines": { 10 | "node": ">=15", 11 | "npm": ">=7" 12 | }, 13 | "scripts": { 14 | "format": "prettier --write **/{src,test}/**/*.{js,ts}", 15 | "clean": "rm -rf {.,reporters,zora,assert,pta}/{node_modules,dist,package-lock.json}", 16 | "install:fresh": "npm i && npm i --workspaces", 17 | "install:ci": "npm ci --include=dev", 18 | "build": "npm run build --workspaces --if-present", 19 | "clean:install": "npm run clean && npm run install:fresh && npm run build", 20 | "test": "npm t --workspaces --if-present" 21 | }, 22 | "devDependencies": { 23 | "@rollup/plugin-commonjs": "^28.0.1", 24 | "@rollup/plugin-node-resolve": "^15.2.3", 25 | "prettier": "^3.1.1", 26 | "rollup": "^4.9.1", 27 | "rollup-plugin-dts": "^6.1.0", 28 | "typescript": "^5.3.3" 29 | }, 30 | "prettier": { 31 | "singleQuote": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /zora/test/samples/nested_async.js: -------------------------------------------------------------------------------- 1 | import { test } from '../../src/index.js'; 2 | 3 | const wait = (time) => 4 | new Promise((resolve) => { 5 | setTimeout(() => resolve(), time); 6 | }); 7 | 8 | test('tester 1', async (t) => { 9 | let counter = 0; 10 | 11 | t.test('update counter with delay', async (t) => { 12 | t.ok(true, 'nested 1'); 13 | await wait(100); 14 | counter++; 15 | t.ok(true, 'nested 2'); 16 | }); 17 | 18 | t.test('check counter', (t) => { 19 | t.equal(counter, 0, 'should see the old value of the counter'); 20 | }); 21 | 22 | t.ok(true, 'assert2'); 23 | }); 24 | 25 | test('tester 2', async (t) => { 26 | let counter = 0; 27 | t.ok(true, 'assert3'); 28 | 29 | await t.test('update counter with delay but blocking', async (t) => { 30 | t.ok(true, 'nested 1'); 31 | await wait(100); 32 | counter++; 33 | t.ok(true, 'nested 2'); 34 | }); 35 | 36 | t.test('check counter bis', (t) => { 37 | t.equal(counter, 1, 'should see the new value of the counter'); 38 | }); 39 | 40 | t.ok(true, 'whatever'); 41 | }); 42 | -------------------------------------------------------------------------------- /assert/src/utils.js: -------------------------------------------------------------------------------- 1 | export const Operator = { 2 | EQUAL: 'equal', 3 | NOT_EQUAL: 'notEqual', 4 | IS: 'is', 5 | OK: 'ok', 6 | NOT_OK: 'notOk', 7 | IS_NOT: 'isNot', 8 | FAIL: 'fail', 9 | THROWS: 'throws', 10 | }; 11 | 12 | const specFnRegexp = /zora_spec_fn/; 13 | const zoraInternal = /zora\/dist/; 14 | const filterStackLine = (l) => 15 | (l && !zoraInternal.test(l) && !l.startsWith('Error')) || 16 | specFnRegexp.test(l); 17 | 18 | const getAssertionLocation = () => { 19 | const err = new Error(); 20 | const stack = (err.stack || '') 21 | .split('\n') 22 | .map((l) => l.trim()) 23 | .filter(filterStackLine); 24 | const userLandIndex = stack.findIndex((l) => specFnRegexp.test(l)); 25 | const stackline = 26 | userLandIndex >= 1 ? stack[userLandIndex - 1] : stack[0] || 'N/A'; 27 | return stackline.replace(/^at|^@/, ''); 28 | }; 29 | 30 | export const decorateWithLocation = (result) => { 31 | if (result.pass === false) { 32 | return { 33 | ...result, 34 | at: getAssertionLocation(), 35 | }; 36 | } 37 | return result; 38 | }; 39 | -------------------------------------------------------------------------------- /perfs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "perfs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "perf:clean": "rm -r ./suites && mkdir -p ./suites/{zora,ava,mocha,tape,uvu}/test/ ./suites/jest/__tests__", 9 | "perf:zora": "time node ./suites/zora/index.js", 10 | "perf:pta": "time pta ./suites/zora/test", 11 | "perf:ava": "time ava ./suites/ava/test/*.js", 12 | "perf:mocha": "time mocha ./suites/mocha/test/", 13 | "perf:mocha:parallel": "time mocha --parallel ./suites/mocha/test/", 14 | "perf:tape": "time tape ./suites/tape/test/*.js", 15 | "perf:jest": "time jest --env=node suites/jest/", 16 | "perf:uvu": "time uvu ./suites/uvu/test/" 17 | }, 18 | "prettier": { 19 | "singleQuote": true 20 | }, 21 | "author": "", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "arg": "^5.0.2", 25 | "ava": "^6.0.1", 26 | "jest": "^29.7.0", 27 | "mocha": "^10.2.0", 28 | "pta": "*", 29 | "tape": "^5.7.2", 30 | "uvu": "^0.5.6", 31 | "zora": "*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 RENARD Laurent 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 | -------------------------------------------------------------------------------- /reporters/src/diff/theme.js: -------------------------------------------------------------------------------- 1 | import * as colors from 'colorette'; 2 | import { compose } from '../utils.js'; 3 | import { bold, underline } from 'colorette'; 4 | import { withMargin } from './utils.js'; 5 | 6 | const badge = (fn) => compose([fn, bold, withMargin, String]); 7 | 8 | export const createTheme = ({ 9 | bgError = colors.bgRed, 10 | bgSuccess = colors.bgGreen, 11 | bgSkip = colors.bgYellow, 12 | disableFont = colors.gray, 13 | badgeFont = colors.whiteBright, 14 | adornerFont = colors.cyan, 15 | } = {}) => { 16 | const success = compose([bgSuccess, badgeFont]); 17 | const error = compose([bgError, badgeFont]); 18 | return { 19 | errorBadge: badge(error), 20 | successBadge: badge(success), 21 | skipBadge: badge(compose([bgSkip, badgeFont])), 22 | disable: compose([disableFont, withMargin]), 23 | header: bold, 24 | adorner: adornerFont, 25 | emphasis: compose([underline, bold]), 26 | operator: adornerFont, 27 | light: disableFont, 28 | diffExpected: success, 29 | diffActual: error, 30 | }; 31 | }; 32 | 33 | export const dark = createTheme(); 34 | 35 | export const light = createTheme(); 36 | -------------------------------------------------------------------------------- /zora/test/samples/timeout.txt: -------------------------------------------------------------------------------- 1 | TAP version 13 2 | # broken promise 3 | not ok 1 - The test did no complete on time. refer to https://github.com/lorenzofox3/zora/tree/master/zora#test-timeout for more info 4 | --- 5 | operator: "timeout" 6 | actual: "test takes longer than 500ms to complete" 7 | expected: "test takes less than 500ms to complete" 8 | ... 9 | # timeout in a nested test 10 | # nested 11 | not ok 2 - The test did no complete on time. refer to https://github.com/lorenzofox3/zora/tree/master/zora#test-timeout for more info 12 | --- 13 | operator: "timeout" 14 | actual: "test takes longer than 500ms to complete" 15 | expected: "test takes less than 500ms to complete" 16 | ... 17 | # just too long 18 | # on time 19 | ok 3 - should be truthy 20 | # too late 21 | not ok 4 - The test did no complete on time. refer to https://github.com/lorenzofox3/zora/tree/master/zora#test-timeout for more info 22 | --- 23 | operator: "timeout" 24 | actual: "test takes longer than 500ms to complete" 25 | expected: "test takes less than 500ms to complete" 26 | ... 27 | 28 | 1..4 29 | # tests 4 30 | # pass 1 31 | # fail 3 32 | # skip 0 33 | -------------------------------------------------------------------------------- /reporters/src/diff/writer/summary.js: -------------------------------------------------------------------------------- 1 | const hasSome = (label) => (counter) => counter[label] > 0; 2 | const hasFailure = hasSome('failure'); 3 | const hasSkip = hasSome('skip'); 4 | 5 | const getPad = 6 | ({ total }) => 7 | (number) => 8 | String(number).padStart(String(total).length + 2); 9 | 10 | export default ({ theme }) => { 11 | return { 12 | fail(counter) { 13 | const padNumber = getPad(counter); 14 | const label = `FAIL:${padNumber(counter.failure)}`; 15 | return hasFailure(counter) 16 | ? theme.errorBadge(label) 17 | : theme.disable(label); 18 | }, 19 | pass(counter) { 20 | const padNumber = getPad(counter); 21 | const label = `PASS:${padNumber(counter.success)}`; 22 | return hasFailure(counter) 23 | ? theme.disable(label) 24 | : theme.successBadge(label); 25 | }, 26 | skip(counter) { 27 | const padNumber = getPad(counter); 28 | const label = `SKIP:${padNumber(counter.skip)}`; 29 | return hasSkip(counter) ? theme.skipBadge(label) : theme.disable(label); 30 | }, 31 | total(counter) { 32 | return theme.header(`TOTAL: ${counter.total}`); 33 | }, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /zora/src/index.js: -------------------------------------------------------------------------------- 1 | import { createHarness as privateHarnessFactory } from './harness.js'; 2 | import { findConfigurationValue, isBrowser } from './env.js'; 3 | import { createJSONReporter, createTAPReporter } from 'zora-reporters'; 4 | 5 | let autoStart = true; 6 | 7 | const harness = privateHarnessFactory({ 8 | onlyMode: findConfigurationValue('ZORA_ONLY') !== void 0, 9 | }); 10 | 11 | export { Assert } from './test.js'; 12 | export { createJSONReporter, createTAPReporter } from 'zora-reporters'; 13 | 14 | export const only = harness.only; 15 | 16 | export const test = harness.test; 17 | 18 | export const skip = harness.skip; 19 | 20 | export const report = harness.report; 21 | 22 | export const hold = () => !(autoStart = false); 23 | 24 | export const createHarness = (opts) => { 25 | hold(); 26 | return privateHarnessFactory(opts); 27 | }; 28 | 29 | const start = async () => { 30 | if (autoStart) { 31 | const reporter = 32 | findConfigurationValue('ZORA_REPORTER') === 'json' 33 | ? createJSONReporter() 34 | : createTAPReporter(); 35 | await report({ reporter }); 36 | } 37 | }; 38 | 39 | // on next tick start reporting 40 | if (!isBrowser) { 41 | setTimeout(start, 0); 42 | } else { 43 | window.addEventListener('load', start); 44 | } 45 | -------------------------------------------------------------------------------- /reporters/src/counter.js: -------------------------------------------------------------------------------- 1 | import { MESSAGE_TYPE } from './protocol.js'; 2 | import { isSkipped } from './utils.js'; 3 | 4 | const idSequence = () => { 5 | let id = 0; 6 | return () => ++id; 7 | }; 8 | 9 | export const createCounter = () => { 10 | const nextId = idSequence(); 11 | let success = 0; 12 | let failure = 0; 13 | let skip = 0; 14 | 15 | return Object.create( 16 | { 17 | increment(message) { 18 | const { type } = message; 19 | if (isSkipped(message)) { 20 | skip++; 21 | } else if (type === MESSAGE_TYPE.ASSERTION) { 22 | success += message.data.pass === true ? 1 : 0; 23 | failure += message.data.pass === false ? 1 : 0; 24 | } 25 | }, 26 | nextId, 27 | }, 28 | { 29 | success: { 30 | enumerable: true, 31 | get() { 32 | return success; 33 | }, 34 | }, 35 | failure: { 36 | enumerable: true, 37 | get() { 38 | return failure; 39 | }, 40 | }, 41 | skip: { 42 | enumerable: true, 43 | get() { 44 | return skip; 45 | }, 46 | }, 47 | total: { 48 | enumerable: true, 49 | get() { 50 | return skip + failure + success; 51 | }, 52 | }, 53 | } 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /reporters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zora-reporters", 3 | "version": "2.0.0", 4 | "description": "a set of reporter for zora testing library", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "bin": { 8 | "zr": "./src/bin.js" 9 | }, 10 | "exports": { 11 | "./package.json": "./package.json", 12 | ".": { 13 | "import": { 14 | "default": "./dist/index.js", 15 | "types": "./dist/index.d.ts" 16 | }, 17 | "require": { 18 | "default": "./dist/index.cjs", 19 | "types": "./dist/index.d.cts" 20 | } 21 | } 22 | }, 23 | "scripts": { 24 | "test": "pta ./src/*.test.js ./src/**/*.test.js ./src/diff/*.test.js ./src/diff/**/*.test.js", 25 | "build:types": "rollup -c rollup-types.js", 26 | "build:cjs": "rollup -c rollup.js", 27 | "build": "npm run build:types && npm run build:cjs" 28 | }, 29 | "files": [ 30 | "src", 31 | "dist", 32 | "!*.test.js", 33 | "!src/types" 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/lorenzofox3/zora.git" 38 | }, 39 | "author": "@lorenzofox3 ", 40 | "license": "MIT", 41 | "prettier": { 42 | "singleQuote": true 43 | }, 44 | "dependencies": { 45 | "arg": "^5.0.2", 46 | "colorette": "^2.0.20", 47 | "diff": "^7.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your willingness to contribute ! 4 | 5 | ## Prerequisite 6 | 7 | Before implementing anything, please make sure there is an open ticket referring to the bug you are fixing or the wanted feature you are implementing. That's easier for us to track things and make sure we are adding code to something _useful_ 8 | 9 | However, if your intention is to improve the documentation, there is no need to open a ticket. 10 | 11 | ## Code 12 | 13 | You'll need [nodejs](https://nodejs.org/en/) (version >= 15) installed on your machine. You will need npm (version >= 7) too as we are using npm workspaces. 14 | 15 | 1. clone the project 16 | 2. run ``npm run clean:install && npm run build`` 17 | 3. you can make a quick check everything is ok by running ``npm t`` 18 | 19 | This is a monorepo with various _sub packages_. Most of the time, you will work on a project at the same time, and they come with a dev script. 20 | 21 | For example, if you are working on the assertion library: 22 | 1. ``cd assert`` 23 | 2. ``npm run dev`` 24 | 25 | Once you are done, make sure the integration does not break anything by running the build script and the tests at the root of the project. 26 | 27 | ## Questions 28 | 29 | If you have any further question, please contact us through the Github discussions. 30 | 31 | Thanks again ! 32 | 33 | 34 | -------------------------------------------------------------------------------- /reporters/src/utils.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { defaultSerializer as stringify } from './utils.js'; 3 | 4 | test(`serialize`, ({ test }) => { 5 | test('literals', ({ eq }) => { 6 | eq(stringify(4), '4'); 7 | eq(stringify('foo'), '"foo"'); 8 | eq(stringify(null), 'null'); 9 | eq(stringify(undefined), undefined); 10 | }); 11 | 12 | test(`simple object`, ({ eq }) => { 13 | eq(stringify({ foo: 'bar' }), `{"foo":"bar"}`); 14 | }); 15 | 16 | test(`simple array`, ({ eq }) => { 17 | eq(stringify([{ foo: 'bar' }, 4]), `[{"foo":"bar"},4]`); 18 | }); 19 | 20 | test(`symbols`, ({ eq }) => { 21 | eq(stringify(Symbol('some symbol')), '"Symbol(some symbol)"'); 22 | eq( 23 | stringify({ foo: Symbol('some symbol') }), 24 | '{"foo":"Symbol(some symbol)"}', 25 | 'nested' 26 | ); 27 | }); 28 | 29 | test(`circular dependencies`, ({ eq }) => { 30 | const a = { 31 | foo: 'bar', 32 | }; 33 | const b = { 34 | foo: 'other bar', 35 | a, 36 | }; 37 | 38 | a.b = b; 39 | 40 | eq( 41 | stringify(a), 42 | '{"foo":"bar","b":{"foo":"other bar","a":"[__CIRCULAR_REF__]"}}' 43 | ); 44 | 45 | delete b.a; 46 | 47 | eq( 48 | stringify(a), 49 | '{"foo":"bar","b":{"foo":"other bar"}}', 50 | 'should not keep in memory visited node' 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /zora/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zora", 3 | "version": "6.0.0", 4 | "description": "the lightest yet the fastest javascript testing library", 5 | "keywords": [ 6 | "test", 7 | "testing", 8 | "unit", 9 | "tap", 10 | "assertion", 11 | "assert", 12 | "tdd", 13 | "bdd", 14 | "ava", 15 | "tape", 16 | "jest", 17 | "uvu", 18 | "mocha" 19 | ], 20 | "type": "module", 21 | "main": "./dist/index.cjs", 22 | "exports": { 23 | "./package.json": "./package.json", 24 | ".": { 25 | "import": { 26 | "types": "./dist/index.d.ts", 27 | "default": "./dist/index.js" 28 | }, 29 | "require": { 30 | "types": "./dist/index.d.cts", 31 | "default": "./dist/index.cjs" 32 | } 33 | } 34 | }, 35 | "prettier": { 36 | "singleQuote": true 37 | }, 38 | "scripts": { 39 | "build:types": "rollup -c rollup-types.js", 40 | "build:js": "rollup -c rollup.js", 41 | "build": "npm run build:js && npm run build:types", 42 | "build:watch": "rollup -w -c rollup.js", 43 | "test": "node test/run.js" 44 | }, 45 | "devDependencies": { 46 | "zora-assert": "*" 47 | }, 48 | "files": [ 49 | "dist" 50 | ], 51 | "author": "@lorenzofox3 ", 52 | "license": "MIT", 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/lorenzofox3/zora.git" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /reporters/src/diff/utils.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { leftPad, rightPad, typeAsString, withMargin } from './utils.js'; 3 | 4 | test('utils', (t) => { 5 | t.test(`typeAsString`, (t) => { 6 | t.test(`literals`, (t) => { 7 | t.eq(typeAsString('some string'), 'string'); 8 | t.eq(typeAsString(42), 'number'); 9 | t.eq(typeAsString(true), 'boolean'); 10 | t.eq(typeAsString(undefined), 'undefined'); 11 | }); 12 | 13 | t.test(`objects`, (t) => { 14 | t.eq(typeAsString({}), 'Object'); 15 | t.eq(typeAsString([]), 'Array'); 16 | t.eq(typeAsString(new Date()), 'Date'); 17 | t.eq(typeAsString(new Map()), 'Map'); 18 | t.eq(typeAsString(new Set()), 'Set'); 19 | t.eq(typeAsString(new (class Foo {})()), 'Foo'); 20 | t.eq(typeAsString(null), 'null'); 21 | }); 22 | }); 23 | 24 | t.test(`left pad`, (t) => { 25 | t.eq(leftPad(4, 'fo'), ' fo'); 26 | const padTwo = leftPad(2); 27 | t.eq(padTwo('fo'), ' fo', 'partial application'); 28 | t.eq(padTwo('bar'), ' bar', 'partial application'); 29 | }); 30 | 31 | t.test(`right pad`, (t) => { 32 | t.eq(rightPad(4, 'fo'), 'fo '); 33 | const padTwo = rightPad(2); 34 | t.eq(padTwo('fo'), 'fo ', 'partial application'); 35 | t.eq(padTwo('bar'), 'bar ', 'partial application'); 36 | }); 37 | 38 | t.test(`with margin`, (t) => { 39 | t.eq(withMargin('some text'), ' some text '); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /reporters/src/diff/index.js: -------------------------------------------------------------------------------- 1 | import { createCounter } from '../counter.js'; 2 | import { createStack } from './stack.js'; 3 | import { createWriter } from './writer/index.js'; 4 | import { eventuallySetExitCode, isAssertionFailing } from '../utils.js'; 5 | import { MESSAGE_TYPE } from '../protocol.js'; 6 | 7 | const writeMessage = ({ writer, stack }) => { 8 | const writeTable = { 9 | [MESSAGE_TYPE.TEST_START](message) { 10 | stack.push(message.data.description); 11 | }, 12 | [MESSAGE_TYPE.TEST_END]() { 13 | stack.pop(); 14 | }, 15 | [MESSAGE_TYPE.ASSERTION](message) { 16 | if (isAssertionFailing(message)) { 17 | writer.printFailingTestPath(stack); 18 | if (message.data.at) { 19 | writer.printLocation(message.data.at); 20 | } 21 | writer.printDiagnostic(message.data); 22 | } 23 | }, 24 | [MESSAGE_TYPE.ERROR](message) { 25 | // todo 26 | throw message.data.error; 27 | }, 28 | }; 29 | 30 | return (message) => writeTable[message.type]?.(message); 31 | }; 32 | 33 | export default () => async (messageStream) => { 34 | const counter = createCounter(); 35 | const stack = createStack(); 36 | const writer = createWriter(); 37 | const write = writeMessage({ writer, stack }); 38 | 39 | for await (const message of messageStream) { 40 | write(message); 41 | counter.increment(message); 42 | eventuallySetExitCode(message); 43 | } 44 | writer.printSummary(counter); 45 | }; 46 | -------------------------------------------------------------------------------- /reporters/src/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { fileURLToPath } from 'url'; 3 | import { dirname, resolve } from 'path'; 4 | import { EOL } from 'os'; 5 | import arg from 'arg'; 6 | import { createTAPReporter, createDiffReporter } from './index.js'; 7 | import { createReadStream } from 'fs'; 8 | import { compose, filter, map, split } from './utils.js'; 9 | import { createJSONParser } from './message-parser.js'; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = dirname(__filename); 13 | 14 | const reporterMap = { 15 | diff: createDiffReporter({}), 16 | tap: createTAPReporter({}), 17 | }; 18 | 19 | const { 20 | ['--reporter']: reporter = 'diff', 21 | ['--help']: help, 22 | ['--strict-mode']: strictMode = false, 23 | } = arg({ 24 | ['--help']: Boolean, 25 | ['--reporter']: String, 26 | ['--strict-mode']: Boolean, 27 | ['-R']: '--reporter', 28 | }); 29 | 30 | const splitLines = split(EOL); 31 | const compact = filter(Boolean); 32 | 33 | (async () => { 34 | if (help) { 35 | createReadStream(resolve(__dirname, './usage.txt')).pipe(process.stdout); 36 | return; 37 | } 38 | 39 | const parse = createJSONParser({ 40 | strictMode, 41 | }); 42 | 43 | const getInputStream = compose([parse, compact, splitLines]); 44 | 45 | const inputStream = getInputStream(process.stdin); 46 | 47 | const report = reporterMap[reporter]; 48 | 49 | if (!report) { 50 | throw new Error(`unknown reporter "${reporter}"`); 51 | } 52 | 53 | await report(inputStream); 54 | })(); 55 | -------------------------------------------------------------------------------- /zora/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IReporter, Message } from '../../../reporters/src/types/index'; 2 | import { IAssert as GenericAssertionAPI } from '../../../assert/src/index'; 3 | 4 | export interface IReportOptions { 5 | reporter: IReporter; 6 | } 7 | 8 | export interface IHarnessOptions { 9 | onlyMode?: boolean; 10 | } 11 | 12 | export interface ITester { 13 | test: ITestFunction; 14 | skip: ITestFunction; 15 | only: ITestFunction; 16 | } 17 | 18 | export interface IAssert extends GenericAssertionAPI, ITester {} 19 | 20 | export interface ISpecFunction { 21 | (assert: IAssert): any; 22 | } 23 | 24 | export interface ITestOptions { 25 | skip?: boolean; 26 | timeout?: number; 27 | } 28 | 29 | export interface ITestFunction { 30 | ( 31 | description: string, 32 | spec: ISpecFunction, 33 | opts?: ITestOptions 34 | ): Promise & AsyncIterable; 35 | } 36 | 37 | export interface ITestHarness extends ITester { 38 | report(opts: IReportOptions): ReturnType; 39 | } 40 | 41 | export { 42 | createJSONReporter, 43 | createTAPReporter, 44 | ILogOptions, 45 | } from '../../../reporters/src/types/index'; 46 | 47 | export let Assert: IAssert; 48 | export declare function hold(): void; 49 | export declare function report(opts: IReportOptions): ReturnType; 50 | export declare function createHarness(opts: IHarnessOptions): ITestHarness; 51 | export declare const test: ITestFunction; 52 | export declare const only: ITestFunction; 53 | export declare const skip: ITestFunction; 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Zora 2 | 3 | All the projects related to [zora](./zora), the testing library 4 | 5 | * [zora](./zora): the testing library 6 | * [zora-assert](./assert): the assertion library behind zora(unpublished) 7 | * [zora-reporters](./reporters): a set of reporters (can be used as a CLI) 8 | * [pta](./pta): A test runner (used with a Command Line Interface) for Nodejs environment 9 | 10 | The repo also comes with 11 | * [a pseudo benchmark](./perfs) which shows the testing experience with various testing framework 12 | * [a list of recipes](./examples) to get started with different environments (node, browser, typescript, etc) 13 | 14 | ## Goals 15 | 16 | Zora is one of the lightest (if not the lightest), yet one of the fastest Javascript testing library (if not the fastest). 17 | 18 | Its design principles follow the line: 19 | 20 | * Runs with any Javascript environment ([Nodejs](https://nodejs.org/en/), [Deno](https://deno.land/), Browser ): you don't need any specific test runner to run your testing program, it is _just_ a regular javascript program 21 | * Is fast and simple: a [small codebase](https://packagephobia.com/result?p=zora) achieving the [best performances](./perfs) to deliver the best developer experience 22 | * Follows the [UNIX philosophy](https://en.wikipedia.org/wiki/Unix_philosophy): a set of focused, composable small software to deliver the best flexibility with the minimum overhead, rather than a huge monolith hard to tweak, with a large set options. 23 | 24 | ## Contributing 25 | 26 | If you wish to contribute to the project, please refer to the [guidelines](./contributing.md) 27 | -------------------------------------------------------------------------------- /examples/node-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-ts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "engines": { 10 | "node": "16" 11 | }, 12 | "scripts": { 13 | "build": "tsc", 14 | "build:watch": "tsc --watch --preserveWatchOutput", 15 | "test": "node -r source-map-support/register dist/test/index.js", 16 | "test:coverage": "c8 --all -x dist/test -x test node -r source-map-support/register dist/test/index.js", 17 | "test:pretty": "ZORA_REPORTER=json node -r source-map-support/register dist/test/index.js | zr", 18 | "test:pta":"node -r source-map-support/register node_modules/.bin/pta dist/test/specs/*.js", 19 | "test:tap-pipeline": "node -r source-map-support/register dist/test/index.js | tap-difflet", 20 | "test:watch": "nodemon -r source-map-support/register --watch dist/ dist/test/index.js", 21 | "test:pta:watch": "nodemon -r source-map-support/register --watch dist/ node_modules/.bin/pta dist/test/specs/*.js", 22 | "dev": "run-p build:watch test:watch", 23 | "dev:pta": "run-p build:watch test:pta:watch" 24 | }, 25 | "author": "", 26 | "license": "ISC", 27 | "devDependencies": { 28 | "@types/is-even": "~1.0.2", 29 | "c8": "~8.0.1", 30 | "nodemon": "~3.0.2", 31 | "npm-run-all": "~4.1.5", 32 | "pta": "*", 33 | "source-map-support": "~0.5.21", 34 | "tap-difflet": "~0.7.2", 35 | "typescript": "~5.3.3", 36 | "zora": "*", 37 | "zora-reporters": "*" 38 | }, 39 | "dependencies": { 40 | "is-even": "~1.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /reporters/src/diff/writer/index.js: -------------------------------------------------------------------------------- 1 | import { compose, defaultLogger } from '../../utils.js'; 2 | import { createTheme } from '../theme.js'; 3 | import { leftPad, withMargin } from '../utils.js'; 4 | import getDiagnosticMessage from './diagnostic.js'; 5 | import getSummaryMessage from './summary.js'; 6 | 7 | export const createWriter = ({ 8 | log = defaultLogger, 9 | theme = createTheme(), 10 | } = {}) => { 11 | const print = compose([log, leftPad(2)]); 12 | 13 | const diagnostics = getDiagnosticMessage({ theme }); 14 | const summary = getSummaryMessage({ theme }); 15 | 16 | const printDiagnostic = (diag) => { 17 | const { description, operator } = diag; 18 | print(''); 19 | const operatorString = theme.operator(`[${operator}]`); 20 | print(description); 21 | print(`${operatorString} ${diagnostics(diag)}`); 22 | }; 23 | 24 | const printSummary = (counter) => { 25 | print(''); 26 | print(summary.total(counter)); 27 | print(summary.pass(counter)); 28 | print(summary.fail(counter)); 29 | print(summary.skip(counter)); 30 | print(''); 31 | }; 32 | 33 | const printLocation = (at) => print(`${theme.light('at')}: ${at}`); 34 | 35 | const printFailingTestPath = (stack) => { 36 | print(''); 37 | const testPath = [...stack]; 38 | const current = testPath.pop(); 39 | print( 40 | `${theme.errorBadge('FAIL')} ${theme.header( 41 | [...testPath, theme.emphasis(current)].join( 42 | theme.adorner(withMargin('>')) 43 | ) 44 | )}` 45 | ); 46 | }; 47 | 48 | return { 49 | printDiagnostic, 50 | print, 51 | printSummary, 52 | printFailingTestPath, 53 | printLocation, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /perfs/readme.md: -------------------------------------------------------------------------------- 1 | # perfs 2 | 3 | A program that aims at giving an idea on how fast common testing libraries/framework can run a similar test suite 4 | 5 | 1. install the dependencies ``npm install`` 6 | 2. clean the suites ``npm run perf:clean`` 7 | 3. generate a profile ``node scripts/test-files-io.js --files 10 --tests 8 --idle 20`` 8 | 9 | Where: 10 | * files: the number of files in the suites 11 | * tests: the number of tests in a file 12 | * idle: the time (in ms) a given test is idle before resuming 13 | 14 | A test would be the equivalent with each library/framework 15 | 16 | ```javascript 17 | import {test} from 'zora'; 18 | 19 | test('test ' + i, async function (assert) { 20 | await new Promise(resolve => { 21 | setTimeout(()=>resolve(),${idleTiem}); 22 | }); 23 | assert.ok(Math.random() * 100 > 5); // 5% of the tests should fail 24 | }); 25 | ``` 26 | 27 | This test should represent "common" javascript code: non-blocking IO 28 | 29 | ## Results 30 | 31 | On my machine (MacBook Pro, 2.7 GHz i5, 8 GB RAM) for various profiles I get: 32 | 33 | Library: files=5, tests=8, idle=5 34 | Small Web app: files=10, tests=10, idle=30 35 | Web app: files=100, tests=10, idle=30 36 | 37 | | | zora@next | with pta@next | tape@5.7.2 | Jest@29.7.0 | AvA@6.0.1 | Mocha@10.2.0 | uvu@0.5.6 38 | |--------|:---------:|:-------------:|:----------:|:-----------:|:---------:|:------------:|:---------:| 39 | |Library | 124ms | 204ms | 511ms | 2550ms | 1705ms | 582ms | 369ms | 40 | |small web app | 122ms | 208ms | 555ms | 1853ms | 2616ms | 767ms | 294ms | 41 | |web app | 608ms | 475ms | 3521ms | 11780ms | 35320ms | 2050ms | 1790ms | 42 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/equal.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { diffLine, expandNewLines, getDiffCharThemedMessage } from './equal.js'; 3 | 4 | const theme = new Proxy( 5 | {}, 6 | { 7 | get: function (target, prop, receiver) { 8 | return (val) => `<${prop}>${val}`; 9 | }, 10 | } 11 | ); 12 | 13 | test(`getDiffCharThemedMessage`, (t) => { 14 | const getMessage = getDiffCharThemedMessage(theme); 15 | const { expected, actual } = getMessage({ 16 | actual: 'fob', 17 | expected: 'foo', 18 | }); 19 | t.eq(expected, 'foo'); 20 | t.eq(actual, 'fob'); 21 | }); 22 | 23 | test('getDiffJSONThemedMessage', (t) => { 24 | t.test(`expandNewLines`, (t) => { 25 | const lines = [ 26 | { value: '{\n "foo": "bar",\n' }, 27 | { 28 | removed: true, 29 | value: ' "other": "waht"\n', 30 | }, 31 | { 32 | added: true, 33 | value: ' "other": "what"\n', 34 | }, 35 | { value: '}' }, 36 | ]; 37 | 38 | const actual = expandNewLines(lines); 39 | 40 | t.eq(actual, [ 41 | { value: '{' }, 42 | { value: ' "foo": "bar",' }, 43 | { 44 | removed: true, 45 | value: ' "other": "waht"', 46 | }, 47 | { added: true, value: ' "other": "what"' }, 48 | { value: '}' }, 49 | ]); 50 | }); 51 | 52 | t.test(`diffLine`, (t) => { 53 | const diff = diffLine(theme); 54 | t.eq( 55 | diff({ added: true, value: 'foo' }), 56 | '+ foo' 57 | ); 58 | t.eq( 59 | diff({ removed: true, value: 'foo' }), 60 | '- foo' 61 | ); 62 | t.eq(diff({ value: 'foo' }), ' foo'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /reporters/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IAssertionResult } from '../../../assert/src/index'; 2 | 3 | interface INewTestMessageInput { 4 | description: string; 5 | skip: boolean; 6 | } 7 | 8 | interface ITestEndMessageInput { 9 | description: string; 10 | executionTime: number; 11 | } 12 | 13 | interface IMessage { 14 | type: string; 15 | data: T; 16 | } 17 | 18 | interface INewTestMessage extends IMessage { 19 | type: 'TEST_START'; 20 | } 21 | 22 | interface IAssertionMessage extends IMessage> { 23 | type: 'ASSERTION'; 24 | } 25 | 26 | interface ITestEndMessage extends IMessage { 27 | type: 'TEST_END'; 28 | } 29 | 30 | interface IErrorMessage extends IMessage<{ error: unknown }> { 31 | type: 'ERROR'; 32 | } 33 | 34 | export type Message = 35 | | IAssertionMessage 36 | | IErrorMessage 37 | | ITestEndMessage 38 | | INewTestMessage; 39 | 40 | export declare function newTestMessage( 41 | opts: INewTestMessageInput 42 | ): INewTestMessage; 43 | 44 | export declare function assertionMessage( 45 | opts: IAssertionResult 46 | ): IAssertionMessage; 47 | 48 | export declare function testEndMessage( 49 | opts: INewTestMessageInput 50 | ): ITestEndMessage; 51 | 52 | export declare function errorMessage(opts: { error: unknown }): IErrorMessage; 53 | 54 | export interface IReporter { 55 | (messageStream: AsyncIterable): Promise; 56 | } 57 | 58 | export interface ILogOptions { 59 | log?: (message: any) => void; 60 | serialize?: (value: any) => string; 61 | } 62 | 63 | export declare function createJSONReporter(opts?: ILogOptions): IReporter; 64 | 65 | export declare function createTAPReporter(opts?: ILogOptions): IReporter; 66 | 67 | export declare function createDiffReporter(): IReporter; 68 | -------------------------------------------------------------------------------- /reporters/src/tap/index.js: -------------------------------------------------------------------------------- 1 | import { assertionMessage, MESSAGE_TYPE } from '../protocol.js'; 2 | import { 3 | defaultLogger, 4 | defaultSerializer, 5 | eventuallySetExitCode, 6 | filter, 7 | } from '../utils.js'; 8 | import { createCounter } from '../counter.js'; 9 | import { createWriter } from './writer.js'; 10 | 11 | const isNotTestEnd = ({ type }) => type !== MESSAGE_TYPE.TEST_END; 12 | const filterOutTestEnd = filter(isNotTestEnd); 13 | 14 | const writeMessage = ({ writer, nextId }) => { 15 | const writerTable = { 16 | [MESSAGE_TYPE.ASSERTION](message) { 17 | return writer.printAssertion(message, { id: nextId() }); 18 | }, 19 | [MESSAGE_TYPE.TEST_START](message) { 20 | if (message.data.skip) { 21 | const skippedAssertionMessage = assertionMessage({ 22 | description: message.data.description, 23 | pass: true, 24 | }); 25 | return writer.printAssertion(skippedAssertionMessage, { 26 | comment: 'SKIP', 27 | id: nextId(), 28 | }); 29 | } 30 | return writer.printTestStart(message); 31 | }, 32 | [MESSAGE_TYPE.ERROR](message) { 33 | writer.printBailOut(); 34 | throw message.data.error; 35 | }, 36 | }; 37 | return (message) => writerTable[message.type]?.(message); 38 | }; 39 | 40 | export default ({ log = defaultLogger, serialize = defaultSerializer } = {}) => 41 | async (messageStream) => { 42 | const writer = createWriter({ 43 | log, 44 | serialize, 45 | }); 46 | const counter = createCounter(); 47 | const write = writeMessage({ writer, nextId: counter.nextId }); 48 | const stream = filterOutTestEnd(messageStream); 49 | 50 | writer.printHeader(); 51 | for await (const message of stream) { 52 | counter.increment(message); 53 | write(message); 54 | eventuallySetExitCode(message); 55 | } 56 | writer.printSummary(counter); 57 | }; 58 | -------------------------------------------------------------------------------- /assert/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface IAssertionResult { 2 | pass: boolean; 3 | actual: unknown; 4 | expected: T; 5 | description: string; 6 | operator: string; 7 | at?: string; 8 | } 9 | 10 | export interface ComparatorAssertionFunction { 11 | (actual: unknown, expected: T, description?: string): IAssertionResult; 12 | } 13 | 14 | export interface BooleanAssertionFunction { 15 | (actual: unknown, description?: string): IAssertionResult; 16 | } 17 | 18 | export type ErrorAssertionFunction = { 19 | ( 20 | fn: Function, 21 | expected: RegExp | Function, 22 | description?: string 23 | ): IAssertionResult; 24 | (fn: Function, description?: string): IAssertionResult; 25 | }; 26 | 27 | export interface MessageAssertionFunction { 28 | (message?: string): IAssertionResult; 29 | } 30 | 31 | export interface IAssert { 32 | equal: ComparatorAssertionFunction; 33 | 34 | equals: ComparatorAssertionFunction; 35 | 36 | eq: ComparatorAssertionFunction; 37 | 38 | deepEqual: ComparatorAssertionFunction; 39 | 40 | notEqual: ComparatorAssertionFunction; 41 | 42 | notEquals: ComparatorAssertionFunction; 43 | 44 | notEq: ComparatorAssertionFunction; 45 | 46 | notDeepEqual: ComparatorAssertionFunction; 47 | 48 | is: ComparatorAssertionFunction; 49 | 50 | same: ComparatorAssertionFunction; 51 | 52 | isNot: ComparatorAssertionFunction; 53 | 54 | notSame: ComparatorAssertionFunction; 55 | 56 | ok: BooleanAssertionFunction; 57 | 58 | truthy: BooleanAssertionFunction; 59 | 60 | notOk: BooleanAssertionFunction; 61 | 62 | falsy: BooleanAssertionFunction; 63 | 64 | fail: MessageAssertionFunction; 65 | 66 | throws: ErrorAssertionFunction; 67 | } 68 | 69 | declare function factory(options?: IAssertOptions): IAssert; 70 | 71 | export declare const Assert: IAssert; 72 | 73 | export interface IAssertOptions { 74 | onResult: (result: IAssertionResult) => void; 75 | } 76 | 77 | export default factory; 78 | -------------------------------------------------------------------------------- /reporters/src/tap/writer.js: -------------------------------------------------------------------------------- 1 | import { defaultLogger, defaultSerializer, flatDiagnostic } from '../utils.js'; 2 | 3 | export const createWriter = ({ 4 | log = defaultLogger, 5 | serialize = defaultSerializer, 6 | version = 13, 7 | } = {}) => { 8 | const print = (message, padding = 0) => { 9 | log(message.padStart(message.length + padding * 4)); // 4 white space used as indent 10 | }; 11 | 12 | const printYAML = (obj, padding = 0) => { 13 | const YAMLPadding = padding + 0.5; 14 | print('---', YAMLPadding); 15 | for (const [prop, value] of Object.entries(obj)) { 16 | print(`${prop}: ${serialize(value)}`, YAMLPadding + 0.5); 17 | } 18 | print('...', YAMLPadding); 19 | }; 20 | 21 | const printComment = (comment, padding = 0) => { 22 | print(`# ${comment}`, padding); 23 | }; 24 | 25 | const printBailOut = () => { 26 | print('Bail out! Unhandled error.'); 27 | }; 28 | 29 | const printTestStart = (newTestMessage) => { 30 | const { 31 | data: { description }, 32 | } = newTestMessage; 33 | printComment(description); 34 | }; 35 | 36 | const printAssertion = (assertionMessage, { id, comment = '' }) => { 37 | const { data } = assertionMessage; 38 | const { pass, description } = data; 39 | const label = pass === true ? 'ok' : 'not ok'; 40 | const directiveComment = comment ? ` # ${comment}` : ''; 41 | print(`${label} ${id} - ${description}` + directiveComment); 42 | if (pass === false) { 43 | printYAML(flatDiagnostic(data)); 44 | } 45 | }; 46 | 47 | const printSummary = ({ success, skip, failure, total }) => { 48 | print('', 0); 49 | print(`1..${total}`); 50 | printComment(`tests ${total}`, 0); 51 | printComment(`pass ${success}`, 0); 52 | printComment(`fail ${failure}`, 0); 53 | printComment(`skip ${skip}`, 0); 54 | }; 55 | 56 | const printHeader = () => { 57 | print(`TAP version ${version}`); 58 | }; 59 | 60 | return { 61 | print, 62 | printYAML, 63 | printComment, 64 | printBailOut, 65 | printTestStart, 66 | printAssertion, 67 | printSummary, 68 | printHeader, 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /reporters/src/utils.js: -------------------------------------------------------------------------------- 1 | import { MESSAGE_TYPE } from './protocol.js'; 2 | 3 | const isNode = typeof process !== 'undefined'; 4 | 5 | export const flatDiagnostic = ({ pass, description, ...rest }) => rest; 6 | 7 | const createReplacer = () => { 8 | const visited = new Set(); 9 | return (key, value) => { 10 | if (isObject(value)) { 11 | if (visited.has(value)) { 12 | return '[__CIRCULAR_REF__]'; 13 | } 14 | 15 | visited.add(value); 16 | } 17 | 18 | if (typeof value === 'symbol') { 19 | return value.toString(); 20 | } 21 | 22 | return value; 23 | }; 24 | }; 25 | 26 | const isObject = (candidate) => 27 | typeof candidate === 'object' && candidate !== null; 28 | 29 | const stringify = (value) => JSON.stringify(value, createReplacer()); 30 | 31 | export const defaultSerializer = stringify; 32 | 33 | export const defaultLogger = (value) => console.log(value); 34 | 35 | export const isAssertionFailing = (message) => 36 | message.type === MESSAGE_TYPE.ASSERTION && !message.data.pass; 37 | 38 | export const isSkipped = (message) => 39 | message.type === MESSAGE_TYPE.TEST_START && message.data.skip; 40 | 41 | export const eventuallySetExitCode = (message) => { 42 | if (isNode && isAssertionFailing(message)) { 43 | process.exitCode = 1; 44 | } 45 | }; 46 | 47 | export const compose = (fns) => (arg) => 48 | fns.reduceRight((arg, fn) => fn(arg), arg); 49 | 50 | export const split = (separator) => 51 | async function* (stream) { 52 | let buffer = ''; 53 | for await (const chunk of stream) { 54 | const parts = (buffer + chunk.toString()).split(separator); 55 | buffer = parts.pop(); 56 | yield* parts; 57 | } 58 | 59 | if (buffer) { 60 | yield buffer; 61 | } 62 | }; 63 | 64 | export const filter = (predicate) => 65 | async function* (stream) { 66 | for await (const element of stream) { 67 | if (predicate(element)) { 68 | yield element; 69 | } 70 | } 71 | }; 72 | 73 | export const map = (mapFn) => 74 | async function* (stream) { 75 | for await (const element of stream) { 76 | yield mapFn(element); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /examples/browser-vanilla-vite/bin.js: -------------------------------------------------------------------------------- 1 | import { createServer } from 'vite'; 2 | import { pathToFileURL } from 'url'; 3 | import WebSocket from 'ws'; 4 | import { createDiffReporter } from 'zora-reporters'; 5 | import { compose, map } from './test/util.js'; 6 | 7 | const fileServerPort = process.env.FILE_SERVER_POIRT || 3000; 8 | const webSocketPort = process.env.WS_SERVER_PORT || 8000; 9 | 10 | const locateOnFS = map((message) => { 11 | if (message.data?.pass !== false) { 12 | return message; 13 | } 14 | 15 | const { at, ...restOfData } = message.data; 16 | const { pathname } = new URL(at); 17 | const onFS = new URL(pathname, pathToFileURL(process.cwd())); 18 | 19 | return { 20 | ...message, 21 | data: { 22 | ...restOfData, 23 | at: onFS, 24 | }, 25 | }; 26 | }); 27 | 28 | (async () => { 29 | const reporter = compose([createDiffReporter(), locateOnFS]); 30 | 31 | const fileServer = await createServer({ 32 | server: { 33 | port: fileServerPort, 34 | }, 35 | }); 36 | 37 | const webSocketServer = new WebSocket.Server({ 38 | port: webSocketPort, 39 | }); 40 | 41 | webSocketServer.on('connection', (ws) => { 42 | console.debug('client connected'); 43 | reporter(streamRunFromSocket(ws)); 44 | }); 45 | 46 | webSocketServer.on('close', function close() { 47 | console.debug('client disconnected'); 48 | }); 49 | 50 | await fileServer.listen(); 51 | })(); 52 | 53 | async function* streamRunFromSocket(socket) { 54 | const buffer = []; 55 | let done = false; 56 | let release; 57 | try { 58 | socket.on('message', listener); 59 | 60 | while (true) { 61 | if (done) { 62 | break; 63 | } 64 | const message = buffer.shift(); 65 | if (message) { 66 | yield message; 67 | } else { 68 | await new Promise((resolve) => (release = resolve)); 69 | } 70 | } 71 | } finally { 72 | socket.off('message', listener); 73 | } 74 | 75 | function listener(message) { 76 | const messageObj = JSON.parse(message); 77 | 78 | if (messageObj.type === 'RUN_END') { 79 | done = true; 80 | } 81 | 82 | buffer.push(messageObj); 83 | release?.(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /reporters/readme.md: -------------------------------------------------------------------------------- 1 | # zora-reporters 2 | 3 | A set of reporters to programmatically (or via a CLI) consume a stream made of the zora testing protocol. 4 | 5 | ## installation 6 | 7 | ``npm i zora-reporters`` 8 | 9 | ## zora testing protocol messages 10 | 11 | A zora test harness is actually a stream of messages defined by the following interfaces 12 | 13 | ```typescript 14 | interface IMessage { 15 | type: string; 16 | data: T; 17 | } 18 | 19 | interface INewTestMessage extends IMessage { 20 | type: 'TEST_START'; 21 | } 22 | 23 | interface IAssertionMessage extends IMessage> { 24 | type: 'ASSERTION'; 25 | } 26 | 27 | interface ITestEndMessage extends IMessage { 28 | type: 'TEST_END'; 29 | } 30 | ``` 31 | 32 | ## usage 33 | 34 | ### programmatically 35 | 36 | create the reporter with a factory which you can then pass to the ``report`` function of any test harness (or to the ``report`` function of the default test harness) 37 | 38 | ```javascript 39 | import {hold, report, test} from 'zora'; 40 | import {createDiffReporter} from 'zora-reporters'; 41 | 42 | hold(); // prevent the default test harness to start its reporting 43 | 44 | test(`some test`, (t) => { 45 | t.ok(true) 46 | }); 47 | 48 | // create your custom reporter 49 | const reporter = createDiffReporter(); 50 | 51 | // start reporting 52 | report({reporter}); 53 | ``` 54 | 55 | ### CLI 56 | 57 | Zora testing program will output by default a TAP report, but you can configure it to output the zora testing protocol instead (thanks to the ``ZORA_REPORTER=json`` en variable for example). 58 | 59 | You can then pipe this output into the zora reporter command line interface: 60 | 61 | ``ZORA_REPORTER=json node path/to/testing/program.js | zr`` 62 | 63 | #### Options 64 | 65 | ``` 66 | Synopsis 67 | Transforms a zora testing protocol message stream into a specific reporting format 68 | 69 | Usage 70 | zr 71 | 72 | Options 73 | --reporter, -R One of "tap", "diff"(default). 74 | 75 | Examples 76 | zr -R tap < some/file.txt 77 | ZORA_REPORTER=json node zora/program.js | zr -R diff 78 | ``` 79 | 80 | ## reporters 81 | 82 | ### TAP (Test Anything Protocol) 83 | 84 | ![tap output example](../media/tap.png) 85 | 86 | ### Diff 87 | 88 | The default reporter 89 | 90 | ![default output example](../media/diff.png) 91 | 92 | -------------------------------------------------------------------------------- /examples/browser-vanilla-vite/test/runner.js: -------------------------------------------------------------------------------- 1 | import { hold, report } from 'zora'; 2 | import { map } from './util.js'; 3 | hold(); 4 | 5 | const passThroughReporter = map((message) => message); 6 | 7 | const createSocketReporter = (opts = {}) => 8 | new Promise((resolve, reject) => { 9 | const socket = new WebSocket('ws://localhost:8000'); 10 | // Connection opened 11 | socket.addEventListener('open', function (event) { 12 | resolve(async (stream) => { 13 | sendObj({ type: 'RUN_START' }); 14 | for await (const message of stream) { 15 | sendObj(message); 16 | } 17 | sendObj({ type: 'RUN_END' }); 18 | }); 19 | }); 20 | 21 | function sendObj(obj) { 22 | return socket.send(JSON.stringify(obj)); 23 | } 24 | 25 | socket.addEventListener('error', (err) => { 26 | console.error(err); 27 | reject(err); 28 | }); 29 | }); 30 | 31 | const devToolReporter = async (stream) => { 32 | for await (const message of stream) { 33 | if (message.type === 'ASSERTION') { 34 | if (message.data.pass === true) { 35 | console.log(message); 36 | } else { 37 | console.error(message); 38 | } 39 | } else { 40 | console.info(message); 41 | } 42 | } 43 | }; 44 | 45 | Promise.all( 46 | Object.values(import.meta.glob('./specs/*.js')).map((spec) => spec()) 47 | ) 48 | .then(() => 49 | report({ 50 | reporter: passThroughReporter, 51 | }) 52 | ) 53 | .then(async (messageStream) => { 54 | const [st1, st2] = tee(messageStream); 55 | const socketReporter = await createSocketReporter(); 56 | 57 | socketReporter(st2); 58 | return devToolReporter(st1); 59 | }); 60 | 61 | function iteratorToStream(iterator) { 62 | return new ReadableStream({ 63 | async pull(controller) { 64 | const { value, done } = await iterator.next(); 65 | 66 | if (done) { 67 | controller.close(); 68 | } else { 69 | controller.enqueue(value); 70 | } 71 | }, 72 | }); 73 | } 74 | 75 | async function* streamAsyncIterator(stream) { 76 | const reader = stream.getReader(); 77 | 78 | try { 79 | while (true) { 80 | const { done, value } = await reader.read(); 81 | if (done) return; 82 | yield value; 83 | } 84 | } finally { 85 | reader.releaseLock(); 86 | } 87 | } 88 | 89 | function tee(iterator) { 90 | return iteratorToStream(iterator).tee().map(streamAsyncIterator); 91 | } 92 | -------------------------------------------------------------------------------- /zora/test/run.js: -------------------------------------------------------------------------------- 1 | import { spawn } from 'node:child_process'; 2 | import { resolve, extname } from 'node:path'; 3 | import { readFile } from 'node:fs/promises'; 4 | import { readdirSync } from 'node:fs'; 5 | import { execPath as node } from 'node:process'; 6 | import { test } from '../src/index.js'; 7 | 8 | const ONLY_ERROR = ['no_only_mode.js', 'no_only_mode_nested.js']; 9 | const sampleRoot = resolve(process.cwd(), './test/samples/'); 10 | 11 | const spawnTest = (file) => { 12 | const env = {}; 13 | if (file.startsWith('only')) { 14 | env.ZORA_ONLY = true; 15 | } 16 | return new Promise((resolve) => { 17 | let stdout = ''; 18 | let stderr = ''; 19 | 20 | const cp = spawn(node, [file], { 21 | cwd: sampleRoot, 22 | stdio: ['pipe', 'pipe', 'pipe'], 23 | env, 24 | }); 25 | 26 | cp.stdout.on('data', (chunk) => (stdout += chunk)); 27 | cp.stderr.on('data', (chunk) => (stderr += chunk)); 28 | 29 | cp.on('exit', () => { 30 | resolve({ 31 | stdout: stdout.replace(/at:.*/g, 'at:{STACK}'), 32 | stderr: stderr.replace(/at:.*/g, 'at:{STACK}'), 33 | }); 34 | }); 35 | }); 36 | }; 37 | 38 | const directoryFiles = readdirSync(sampleRoot); 39 | const testCases = directoryFiles.filter( 40 | (f) => 41 | extname(f) === '.js' && !ONLY_ERROR.includes(f) && f !== 'late_collect.js' 42 | ); 43 | 44 | for (const f of testCases) { 45 | test(`testing file ${f}`, async ({ eq }) => { 46 | const { stdout } = await spawnTest(f); 47 | const outputFile = resolve( 48 | sampleRoot, 49 | `${[f.split('.')[0], 'txt'].join('.')}` 50 | ); 51 | const expectedOutput = await readFile(outputFile, { 52 | encoding: 'utf8', 53 | }); 54 | eq(stdout, expectedOutput); 55 | }); 56 | } 57 | 58 | test(`ONLY mode is not set`, ({ eq }) => { 59 | for (const f of directoryFiles.filter((f) => ONLY_ERROR.includes(f))) { 60 | test(`testing file ${f}`, async ({ ok }) => { 61 | const { stderr } = await spawnTest(f); 62 | ok( 63 | stderr.includes( 64 | `Error: Can not use "only" method when not in "run only" mode` 65 | ) 66 | ); 67 | }); 68 | } 69 | }); 70 | 71 | test('testing late_collect.js', async ({ ok }) => { 72 | const { stderr } = await spawnTest('late_collect.js'); 73 | ok( 74 | stderr.includes(`Error: test "late collection" 75 | tried to collect an assertion after it has run to its completion. 76 | You might have forgotten to wait for an asynchronous task to complete`) 77 | ); 78 | }); 79 | -------------------------------------------------------------------------------- /reporters/src/tap/writer.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { createWriter } from './writer.js'; 3 | 4 | const createTestWriter = ({ eq }) => { 5 | const logs = []; 6 | const writer = createWriter({ 7 | log: (message) => logs.push(message), 8 | }); 9 | return { 10 | writer, 11 | check(messages, description) { 12 | eq(logs, messages, description); 13 | }, 14 | }; 15 | }; 16 | 17 | test(`tap writer`, ({ test }) => { 18 | test('print with no padding', ({ eq }) => { 19 | const { writer, check } = createTestWriter({ eq }); 20 | writer.print('test'); 21 | check(['test']); 22 | }); 23 | test('print with padding', ({ eq }) => { 24 | const { writer, check } = createTestWriter({ eq }); 25 | writer.print('test', 2); 26 | check([' test']); 27 | }); 28 | test('printComment with no padding', ({ eq }) => { 29 | const { writer, check } = createTestWriter({ eq }); 30 | writer.printComment('test'); 31 | check(['# test']); 32 | }); 33 | test('printComment with padding', ({ eq }) => { 34 | const { writer, check } = createTestWriter({ eq }); 35 | writer.printComment('test', 1); 36 | check([' # test']); 37 | }); 38 | 39 | test('print header', ({ eq }) => { 40 | const { writer, check } = createTestWriter({ eq }); 41 | writer.printHeader(); 42 | check(['TAP version 13']); 43 | }); 44 | 45 | test('print bailout', ({ eq }) => { 46 | const { writer, check } = createTestWriter({ eq }); 47 | writer.printBailOut(); 48 | check(['Bail out! Unhandled error.']); 49 | }); 50 | 51 | test('print test start', ({ eq }) => { 52 | const { writer, check } = createTestWriter({ eq }); 53 | writer.printTestStart({ data: { description: 'some test' } }); 54 | check(['# some test']); 55 | }); 56 | 57 | test('print assertion', ({ test, skip }) => { 58 | test('simple passing assertion', ({ eq }) => { 59 | const { writer, check } = createTestWriter({ eq }); 60 | writer.printAssertion( 61 | { 62 | data: { 63 | pass: true, 64 | description: 'some test', 65 | }, 66 | }, 67 | { id: 66 } 68 | ); 69 | check(['ok 66 - some test']); 70 | }); 71 | }); 72 | 73 | test('print summary', ({ eq }) => { 74 | const { writer, check } = createTestWriter({ eq }); 75 | writer.printSummary({ 76 | success: 3, 77 | skip: 1, 78 | failure: 5, 79 | total: 9, 80 | }); 81 | check(['', '1..9', '# tests 9', '# pass 3', '# fail 5', '# skip 1']); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /pta/src/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { dirname, resolve } from 'node:path'; 3 | import { fileURLToPath, pathToFileURL } from 'node:url'; 4 | import process from 'node:process'; 5 | import { createReadStream } from 'node:fs'; 6 | 7 | import arg from 'arg'; 8 | import { globby } from 'globby'; 9 | import { 10 | createDiffReporter, 11 | createJSONReporter, 12 | createTAPReporter, 13 | } from 'zora-reporters'; 14 | 15 | const __filename = fileURLToPath(import.meta.url); 16 | const __dirname = dirname(__filename); 17 | 18 | const reporterMap = { 19 | diff: createDiffReporter(), 20 | tap: createTAPReporter(), 21 | json: createJSONReporter(), 22 | log: createJSONReporter(), // alias for "json" 23 | }; 24 | 25 | const DEFAULT_FILE_PATTERNS = [ 26 | '**/test.{js,cjs,mjs}', 27 | '**/*.spec.{js,cjs,mjs}', 28 | '**/*.test.{js,cjs,mjs}', 29 | '**/test/**/*.{js,cjs,mjs}', 30 | '**/tests/**/*.{js,cjs,mjs}', 31 | '**/__tests__/**/*.{js,cjs,mjs}', 32 | '!**/node_modules', 33 | '!node_modules', 34 | ]; 35 | 36 | const { 37 | ['--reporter']: reporter = 'diff', 38 | ['--only']: only = false, 39 | ['--help']: help = false, 40 | ['--timeout']: defaultTimeout = 5000, 41 | _: filePatterns, 42 | } = arg({ 43 | ['--reporter']: String, 44 | ['--only']: Boolean, 45 | ['--help']: Boolean, 46 | ['--timeout']: Number, 47 | // --module-loader is now ignored. 48 | // kept in schema to avoid breaking user's existing usage. 49 | ['--module-loader']: String, 50 | ['-R']: '--reporter', 51 | }); 52 | 53 | (async () => { 54 | if (help) { 55 | createReadStream(resolve(__dirname, './usage.txt')).pipe(process.stdout); 56 | return; 57 | } 58 | 59 | // we set the env var before loading zora 60 | if (only) { 61 | process.env.ZORA_ONLY = true; 62 | } 63 | 64 | process.env.ZORA_TIMEOUT = defaultTimeout; 65 | 66 | // loading zora to hold the singleton 67 | const { hold, report } = await import('zora'); 68 | hold(); 69 | 70 | const reporterInstance = reporterMap[reporter] || reporter.diff; 71 | const files = await globby( 72 | filePatterns.length ? filePatterns : DEFAULT_FILE_PATTERNS 73 | ); 74 | 75 | if (!files.length) { 76 | console.warn(`no file matching the patterns: ${filePatterns.join(', ')}`); 77 | return; 78 | } 79 | 80 | for (const file of files) { 81 | const filePath = resolve(process.cwd(), file); 82 | await import(pathToFileURL(filePath)); // load file in sequence so any top level await allows the tests to run sequentially if needed 83 | } 84 | 85 | await report({ 86 | reporter: reporterInstance, 87 | }); 88 | })(); 89 | -------------------------------------------------------------------------------- /reporters/src/counter.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { createCounter } from './counter.js'; 3 | import { 4 | testEndMessage, 5 | newTestMessage, 6 | errorMessage, 7 | assertionMessage, 8 | } from './protocol.js'; 9 | 10 | test('counter', ({ test }) => { 11 | test('with tests end message, should not increment any value', ({ eq }) => { 12 | const counter = createCounter(); 13 | const message = testEndMessage({ 14 | description: 'example', 15 | executionTime: 42, 16 | }); 17 | counter.increment(message); 18 | eq(counter, { 19 | success: 0, 20 | failure: 0, 21 | skip: 0, 22 | total: 0, 23 | }); 24 | counter.increment(message); 25 | eq(counter, { 26 | success: 0, 27 | failure: 0, 28 | skip: 0, 29 | total: 0, 30 | }); 31 | }); 32 | 33 | test('with error message, should not increment any value', ({ eq }) => { 34 | const counter = createCounter(); 35 | const message = errorMessage({ error: new Error('some error') }); 36 | counter.increment(message); 37 | eq(counter, { 38 | success: 0, 39 | failure: 0, 40 | skip: 0, 41 | total: 0, 42 | }); 43 | counter.increment(message); 44 | eq(counter, { 45 | success: 0, 46 | failure: 0, 47 | skip: 0, 48 | total: 0, 49 | }); 50 | }); 51 | 52 | test('with tests start message, should not increment any value', ({ eq }) => { 53 | const counter = createCounter(); 54 | const message = newTestMessage({ 55 | description: 'example', 56 | skip: false, 57 | }); 58 | counter.increment(message); 59 | eq( 60 | counter, 61 | { 62 | success: 0, 63 | failure: 0, 64 | skip: 0, 65 | total: 0, 66 | }, 67 | 'should not increment the counter' 68 | ); 69 | counter.increment( 70 | newTestMessage({ 71 | description: 'example', 72 | skip: true, 73 | }) 74 | ); 75 | eq( 76 | counter, 77 | { 78 | success: 0, 79 | failure: 0, 80 | skip: 1, 81 | total: 1, 82 | }, 83 | 'should update counter when tests is skipped' 84 | ); 85 | }); 86 | 87 | test('with assertion message, should increment values depending on the test result', ({ 88 | eq, 89 | }) => { 90 | const message = assertionMessage({ 91 | pass: true, 92 | }); 93 | const counter = createCounter(); 94 | counter.increment(message); 95 | eq( 96 | counter, 97 | { success: 1, failure: 0, skip: 0, total: 1 }, 98 | 'when assertion is passing' 99 | ); 100 | counter.increment(message); 101 | eq(counter, { success: 2, failure: 0, skip: 0, total: 2 }); 102 | counter.increment( 103 | assertionMessage({ pass: false }), 104 | 'when assertion is failing' 105 | ); 106 | eq(counter, { success: 2, failure: 1, skip: 0, total: 3 }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /assert/test/factory.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import createAssert, { Assert } from '../src/index.js'; 3 | 4 | const omitAt = ({ at, ...rest }) => rest; 5 | 6 | // use callback 7 | test('use callback', ({ eq }) => { 8 | const assertions = []; 9 | const onResult = (result) => assertions.push(result); 10 | 11 | const assert = createAssert({ onResult }); 12 | 13 | assert.eq('foo', 'foo'); 14 | assert.eq('foo', 'bar'); 15 | 16 | eq(assertions.map(omitAt), [ 17 | { 18 | pass: true, 19 | actual: 'foo', 20 | expected: 'foo', 21 | description: 'should be equivalent', 22 | operator: 'equal', 23 | }, 24 | { 25 | pass: false, 26 | actual: 'foo', 27 | expected: 'bar', 28 | description: 'should be equivalent', 29 | operator: 'equal', 30 | }, 31 | ]); 32 | 33 | eq(Boolean(assertions[0].at), false); 34 | eq( 35 | Boolean(assertions[1].at), 36 | true, 37 | 'should have decorated the result with the location' 38 | ); 39 | }); 40 | 41 | // upgrade API 42 | test('upgrade API', ({ eq }) => { 43 | Assert.isFoo = (expected, description = 'should be foo') => { 44 | return { 45 | pass: expected === 'foo', 46 | expected, 47 | actual: 'foo', 48 | operator: 'isFoo', 49 | description, 50 | }; 51 | }; 52 | 53 | const assertions = []; 54 | const onResult = (result) => assertions.push(result); 55 | 56 | const assert = createAssert({ onResult }); 57 | 58 | assert.isFoo('foo'); 59 | assert.isFoo('bar'); 60 | 61 | eq(assertions.map(omitAt), [ 62 | { 63 | pass: true, 64 | actual: 'foo', 65 | expected: 'foo', 66 | description: 'should be foo', 67 | operator: 'isFoo', 68 | }, 69 | { 70 | pass: false, 71 | actual: 'foo', 72 | expected: 'bar', 73 | description: 'should be foo', 74 | operator: 'isFoo', 75 | }, 76 | ]); 77 | 78 | eq(Boolean(assertions[0].at), false); 79 | eq( 80 | Boolean(assertions[1].at), 81 | true, 82 | 'should have decorated the result with the location' 83 | ); 84 | }); 85 | 86 | // bind functions 87 | test('bind functions', ({ eq }) => { 88 | const assertions = []; 89 | const onResult = (result) => assertions.push(result); 90 | 91 | const { eq: t } = createAssert({ onResult }); 92 | 93 | t('foo', 'foo'); 94 | t('foo', 'bar'); 95 | 96 | eq(assertions.map(omitAt), [ 97 | { 98 | pass: true, 99 | actual: 'foo', 100 | expected: 'foo', 101 | description: 'should be equivalent', 102 | operator: 'equal', 103 | }, 104 | { 105 | pass: false, 106 | actual: 'foo', 107 | expected: 'bar', 108 | description: 'should be equivalent', 109 | operator: 'equal', 110 | }, 111 | ]); 112 | 113 | eq(Boolean(assertions[0].at), false); 114 | eq( 115 | Boolean(assertions[1].at), 116 | true, 117 | 'should have decorated the result with the location' 118 | ); 119 | }); 120 | -------------------------------------------------------------------------------- /assert/src/assert.js: -------------------------------------------------------------------------------- 1 | import { Operator } from './utils.js'; 2 | import eq from 'fast-deep-equal/es6/index.js'; 3 | 4 | export const equal = ( 5 | actual, 6 | expected, 7 | description = 'should be equivalent' 8 | ) => ({ 9 | pass: eq(actual, expected), 10 | actual, 11 | expected, 12 | description, 13 | operator: Operator.EQUAL, 14 | }); 15 | 16 | export const notEqual = ( 17 | actual, 18 | expected, 19 | description = 'should not be equivalent' 20 | ) => ({ 21 | pass: !eq(actual, expected), 22 | actual, 23 | expected, 24 | description, 25 | operator: Operator.NOT_EQUAL, 26 | }); 27 | 28 | export const is = (actual, expected, description = 'should be the same') => ({ 29 | pass: Object.is(actual, expected), 30 | actual, 31 | expected, 32 | description, 33 | operator: Operator.IS, 34 | }); 35 | 36 | export const isNot = ( 37 | actual, 38 | expected, 39 | description = 'should not be the same' 40 | ) => ({ 41 | pass: !Object.is(actual, expected), 42 | actual, 43 | expected, 44 | description, 45 | operator: Operator.IS_NOT, 46 | }); 47 | 48 | export const ok = (actual, description = 'should be truthy') => ({ 49 | pass: Boolean(actual), 50 | actual, 51 | expected: 'truthy value', 52 | description, 53 | operator: Operator.OK, 54 | }); 55 | 56 | export const notOk = (actual, description = 'should be falsy') => ({ 57 | pass: !Boolean(actual), 58 | actual, 59 | expected: 'falsy value', 60 | description, 61 | operator: Operator.NOT_OK, 62 | }); 63 | 64 | export const fail = (description = 'fail called') => ({ 65 | pass: false, 66 | actual: 'fail called', 67 | expected: 'fail not called', 68 | description, 69 | operator: Operator.FAIL, 70 | }); 71 | 72 | export const throws = (func, expected, description = 'should throw') => { 73 | let caught; 74 | let pass; 75 | let actual; 76 | if (typeof expected === 'string') { 77 | [expected, description] = [void 0, expected]; 78 | } 79 | try { 80 | func(); 81 | } catch (err) { 82 | caught = { error: err }; 83 | } 84 | pass = caught !== undefined; 85 | actual = caught?.error; 86 | 87 | if (expected instanceof RegExp) { 88 | pass = expected.test(actual) || expected.test(actual && actual.message); 89 | actual = actual?.message ?? actual; 90 | expected = String(expected); 91 | } else if (typeof expected === 'function' && caught) { 92 | pass = actual instanceof expected; 93 | actual = actual.constructor; 94 | } else { 95 | actual = pass ? 'error thrown' : 'no error thrown'; 96 | } 97 | 98 | return { 99 | pass, 100 | actual, 101 | expected: expected ?? 'any error thrown', 102 | description: description, 103 | operator: Operator.THROWS, 104 | }; 105 | }; 106 | 107 | export const Assert = { 108 | equal, 109 | equals: equal, 110 | eq: equal, 111 | deepEqual: equal, 112 | same: equal, 113 | notEqual, 114 | notEquals: notEqual, 115 | notEq: notEqual, 116 | notDeepEqual: notEqual, 117 | notSame: notEqual, 118 | is, 119 | isNot, 120 | ok, 121 | truthy: ok, 122 | notOk, 123 | falsy: notOk, 124 | fail, 125 | throws, 126 | }; 127 | -------------------------------------------------------------------------------- /zora/src/test.js: -------------------------------------------------------------------------------- 1 | import assertFactory, { Assert } from 'zora-assert'; 2 | import { 3 | assertionMessage, 4 | errorMessage, 5 | newTestMessage, 6 | testEndMessage, 7 | } from 'zora-reporters'; 8 | import { findConfigurationValue } from './env.js'; 9 | 10 | const DEFAULT_TIMEOUT = 5_000; 11 | const defaultOptions = Object.freeze({ 12 | skip: false, 13 | timeout: findConfigurationValue('ZORA_TIMEOUT') || DEFAULT_TIMEOUT, 14 | }); 15 | const noop = () => {}; 16 | 17 | const isTest = (assertionLike) => 18 | assertionLike[Symbol.asyncIterator] !== void 0; 19 | 20 | Assert.test = (description, spec, opts = defaultOptions) => 21 | test(description, spec, opts); 22 | 23 | Assert.skip = (description, spec, opts = defaultOptions) => 24 | test(description, spec, { ...opts, skip: true }); 25 | 26 | Assert.only = () => { 27 | throw new Error(`Can not use "only" method when not in "run only" mode`); 28 | }; 29 | 30 | const createTimeoutResult = ({ timeout }) => ({ 31 | operator: 'timeout', 32 | pass: false, 33 | actual: `test takes longer than ${timeout}ms to complete`, 34 | expected: `test takes less than ${timeout}ms to complete`, 35 | description: 36 | 'The test did no complete on time. refer to https://github.com/lorenzofox3/zora/tree/master/zora#test-timeout for more info', 37 | }); 38 | 39 | export const test = (description, spec, opts = defaultOptions) => { 40 | const { skip = false, timeout = DEFAULT_TIMEOUT } = opts; 41 | const assertions = []; 42 | let executionTime; 43 | let done = false; 44 | let error; 45 | 46 | const onResult = (assertion) => { 47 | if (done) { 48 | throw new Error(`test "${description}" 49 | tried to collect an assertion after it has run to its completion. 50 | You might have forgotten to wait for an asynchronous task to complete 51 | ------ 52 | ${spec.toString()}`); 53 | } 54 | 55 | assertions.push(assertion); 56 | }; 57 | 58 | const specFn = skip 59 | ? noop 60 | : function zora_spec_fn() { 61 | return spec(assertFactory({ onResult })); 62 | }; 63 | 64 | const testRoutine = (async function () { 65 | try { 66 | let timeoutId; 67 | const start = Date.now(); 68 | const result = await Promise.race([ 69 | specFn(), 70 | new Promise((resolve) => { 71 | timeoutId = setTimeout(() => { 72 | onResult(createTimeoutResult({ timeout })); 73 | resolve(); 74 | }, timeout); 75 | }), 76 | ]); 77 | clearTimeout(timeoutId); 78 | executionTime = Date.now() - start; 79 | return result; 80 | } catch (e) { 81 | error = e; 82 | } finally { 83 | done = true; 84 | } 85 | })(); 86 | 87 | return Object.assign(testRoutine, { 88 | [Symbol.asyncIterator]: async function* () { 89 | yield newTestMessage({ description, skip }); 90 | await testRoutine; 91 | for (const assertion of assertions) { 92 | if (isTest(assertion)) { 93 | yield* assertion; 94 | } else { 95 | yield assertionMessage(assertion); 96 | } 97 | } 98 | 99 | if (error) { 100 | yield errorMessage({ error }); 101 | } 102 | 103 | yield testEndMessage({ description, executionTime }); 104 | }, 105 | }); 106 | }; 107 | 108 | export { Assert } from 'zora-assert'; 109 | 110 | export const createAssert = assertFactory; 111 | -------------------------------------------------------------------------------- /reporters/src/message-parser.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { newTestMessage, testEndMessage } from './protocol.js'; 3 | import { createJSONParser } from './message-parser.js'; 4 | 5 | const bufferStream = async (stream) => { 6 | const buffer = []; 7 | for await (const m of stream) { 8 | buffer.push(m); 9 | } 10 | return buffer; 11 | }; 12 | 13 | test('parse message stream in fault tolerant mode', (t) => { 14 | t.test(`should parse zora protocol messages`, async (t) => { 15 | const messageStream = async function* () { 16 | yield JSON.stringify(newTestMessage({ description: 'test start' })); 17 | yield JSON.stringify( 18 | testEndMessage({ description: 'test start', executionTime: 666 }) 19 | ); 20 | }; 21 | 22 | const parse = createJSONParser({ strictMode: false }); 23 | const buffer = await bufferStream(parse(messageStream())); 24 | t.eq(buffer, [ 25 | { type: 'TEST_START', data: { description: 'test start' } }, 26 | { 27 | type: 'TEST_END', 28 | data: { description: 'test start', executionTime: 666 }, 29 | }, 30 | ]); 31 | }); 32 | 33 | t.test( 34 | `should emit a "unknown" message for line the parser can not parse`, 35 | async (t) => { 36 | const messageStream = async function* () { 37 | yield JSON.stringify(newTestMessage({ description: 'test start' })); 38 | yield 'whatever'; 39 | yield JSON.stringify( 40 | testEndMessage({ description: 'test start', executionTime: 666 }) 41 | ); 42 | }; 43 | 44 | const parse = createJSONParser({ strictMode: false }); 45 | const buffer = await bufferStream(parse(messageStream())); 46 | t.eq(buffer, [ 47 | { type: 'TEST_START', data: { description: 'test start' } }, 48 | { type: 'UNKNOWN', data: { message: 'whatever' } }, 49 | { 50 | type: 'TEST_END', 51 | data: { description: 'test start', executionTime: 666 }, 52 | }, 53 | ]); 54 | } 55 | ); 56 | }); 57 | 58 | test('parse message stream in strict mode mode', (t) => { 59 | t.test(`should parse zora protocol messages`, async (t) => { 60 | const messageStream = async function* () { 61 | yield JSON.stringify(newTestMessage({ description: 'test start' })); 62 | yield JSON.stringify( 63 | testEndMessage({ description: 'test start', executionTime: 666 }) 64 | ); 65 | }; 66 | 67 | const parse = createJSONParser({ strictMode: true }); 68 | const buffer = await bufferStream(parse(messageStream())); 69 | t.eq(buffer, [ 70 | { type: 'TEST_START', data: { description: 'test start' } }, 71 | { 72 | type: 'TEST_END', 73 | data: { description: 'test start', executionTime: 666 }, 74 | }, 75 | ]); 76 | }); 77 | 78 | t.test( 79 | `should emit a "unknown" message for line the parser can not parse`, 80 | async (t) => { 81 | const messageStream = async function* () { 82 | yield JSON.stringify(newTestMessage({ description: 'test start' })); 83 | yield 'whatever'; 84 | yield JSON.stringify( 85 | testEndMessage({ description: 'test start', executionTime: 666 }) 86 | ); 87 | }; 88 | 89 | const parse = createJSONParser({ strictMode: true }); 90 | try { 91 | const buffer = await bufferStream(parse(messageStream())); 92 | t.fail('should not reach that code'); 93 | } catch (e) { 94 | t.eq(e.message, 'could not parse line "whatever"'); 95 | } 96 | } 97 | ); 98 | }); 99 | -------------------------------------------------------------------------------- /reporters/src/diff/writer/summary.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import getSummaryMessage from './summary.js'; 3 | 4 | const theme = new Proxy( 5 | {}, 6 | { 7 | get: function (target, prop, receiver) { 8 | return (val) => `<${prop}>${val}`; 9 | }, 10 | } 11 | ); 12 | 13 | test(`summary message`, (t) => { 14 | const { fail, total, pass, skip } = getSummaryMessage({ theme }); 15 | 16 | t.test(`fail`, (t) => { 17 | t.eq( 18 | fail({ 19 | failure: 4, 20 | total: 4, 21 | }), 22 | `FAIL: 4`, 23 | 'when failure count is greater than 0' 24 | ); 25 | 26 | t.eq( 27 | fail({ 28 | failure: 0, 29 | total: 4, 30 | }), 31 | `FAIL: 0`, 32 | 'when failure count is 0' 33 | ); 34 | 35 | t.eq( 36 | fail({ 37 | failure: 4, 38 | total: 10, 39 | }), 40 | `FAIL: 4`, 41 | 'when failure count is greater than 0 and total has one more digit' 42 | ); 43 | 44 | t.eq( 45 | fail({ 46 | failure: 0, 47 | total: 10, 48 | }), 49 | `FAIL: 0`, 50 | 'when failure count is 0 and total has one more digit' 51 | ); 52 | }); 53 | 54 | t.test(`pass`, (t) => { 55 | t.eq( 56 | pass({ 57 | failure: 1, 58 | success: 5, 59 | total: 6, 60 | }), 61 | `PASS: 5`, 62 | 'when failure count is greater than 0' 63 | ); 64 | 65 | t.eq( 66 | pass({ 67 | failure: 0, 68 | success: 4, 69 | total: 4, 70 | }), 71 | `PASS: 4`, 72 | 'when failure count is 0' 73 | ); 74 | 75 | t.eq( 76 | pass({ 77 | failure: 4, 78 | success: 6, 79 | total: 10, 80 | }), 81 | `PASS: 6`, 82 | 'when failure count is greater than 0 and total has one more digit' 83 | ); 84 | 85 | t.eq( 86 | pass({ 87 | failure: 0, 88 | success: 9, 89 | total: 10, 90 | }), 91 | `PASS: 9`, 92 | 'when failure count is 0 and total has one more digit' 93 | ); 94 | }); 95 | 96 | t.test(`skip`, (t) => { 97 | t.eq( 98 | skip({ 99 | failure: 1, 100 | success: 5, 101 | total: 8, 102 | skip: 2, 103 | }), 104 | `SKIP: 2`, 105 | 'when skip count is greater than 0' 106 | ); 107 | 108 | t.eq( 109 | skip({ 110 | failure: 0, 111 | success: 4, 112 | total: 4, 113 | skip: 0, 114 | }), 115 | `SKIP: 0`, 116 | 'when skip count is 0' 117 | ); 118 | 119 | t.eq( 120 | skip({ 121 | failure: 4, 122 | skip: 3, 123 | success: 3, 124 | total: 10, 125 | }), 126 | `SKIP: 3`, 127 | 'when skip count is greater than 0 and total has one more digit' 128 | ); 129 | 130 | t.eq( 131 | skip({ 132 | failure: 1, 133 | skip: 0, 134 | success: 9, 135 | total: 10, 136 | }), 137 | `SKIP: 0`, 138 | 'when skip count is 0 and total has one more digit' 139 | ); 140 | }); 141 | 142 | t.test(`total`, (t) => { 143 | t.eq(total({ total: 4 }), '
TOTAL: 4
'); 144 | t.eq(total({ total: 10 }), '
TOTAL: 10
'); 145 | t.eq(total({ total: 100 }), '
TOTAL: 100
'); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /perfs/scripts/test-files-io.js: -------------------------------------------------------------------------------- 1 | import arg from 'arg'; 2 | import { writeFileSync } from 'fs'; 3 | import { join } from 'path'; 4 | 5 | const range = (number = 1) => 6 | Array.from({ 7 | length: number, 8 | }).map((_, index) => index + 1); 9 | 10 | const { 11 | ['--files']: filesCount = 10, 12 | ['--tests']: testCount = 8, 13 | ['--idle']: waitTime = 30, 14 | } = arg({ 15 | '--files': Number, 16 | '--tests': Number, 17 | '--idle': Number, 18 | }); 19 | 20 | const errorRate = 5; 21 | 22 | const zoraCode = ` 23 | import {test} from 'zora'; 24 | for (let i = 0; i < ${testCount}; i++) { 25 | test('test ' + i, async function (assert) { 26 | await new Promise(resolve => { 27 | setTimeout(()=>resolve(),${waitTime}); 28 | }); 29 | assert.ok(Math.random() * 100 > ${errorRate}); 30 | }); 31 | }; 32 | `; 33 | 34 | const avaCode = ` 35 | import test from 'ava'; 36 | for (let i = 0; i < ${testCount}; i++) { 37 | test('test ' + i, async function (assert) { 38 | await new Promise(resolve => { 39 | setTimeout(()=>resolve(),${waitTime}); 40 | }); 41 | assert.truthy(Math.random() * 100 > ${errorRate}); 42 | }); 43 | } 44 | `; 45 | 46 | const mochaCode = ` 47 | import assert from 'assert'; 48 | describe('test file', function() { 49 | for(let i=0; i < ${testCount};i++){ 50 | it('test ' + i, function(done) { 51 | setTimeout(()=>{ 52 | assert.ok(Math.random() * 100 > ${errorRate}); 53 | done(); 54 | },${waitTime}); 55 | }); 56 | } 57 | }); 58 | `; 59 | 60 | const tapeCode = ` 61 | import test from 'tape'; 62 | for (let i = 0; i < ${testCount}; i++) { 63 | test('test ' + i, function (assert) { 64 | setTimeout(()=>{ 65 | assert.ok(Math.random() * 100 > ${errorRate}); 66 | assert.end(); 67 | },${waitTime}); 68 | }); 69 | } 70 | `; 71 | 72 | const jestCode = ` 73 | describe('add', function () { 74 | for (let i = 0; i < ${testCount}; i++) { 75 | it('should test',async function () { 76 | await new Promise(resolve => { 77 | setTimeout(()=>resolve(),${waitTime}); 78 | }); 79 | expect(Math.random() * 100 > ${errorRate}).toBeTruthy(); 80 | }); 81 | } 82 | }); 83 | `; 84 | 85 | const uvuCode = ` 86 | import { test } from 'uvu'; 87 | import * as assert from 'uvu/assert'; 88 | 89 | for (let i = 0; i < ${testCount}; i++) { 90 | test('test ' + i, async function () { 91 | await new Promise(resolve => { 92 | setTimeout(()=>resolve(),${waitTime}); 93 | }); 94 | assert.ok(Math.random() * 100 > ${errorRate}); 95 | }); 96 | } 97 | 98 | test.run(); 99 | `; 100 | const zoraIndex = ` 101 | ${range(filesCount) 102 | .map((i) => `import './test/test${i}.js'`) 103 | .join(';\n')} 104 | `; 105 | 106 | for (let i = 1; i <= filesCount; i++) { 107 | const zoraPath = join( 108 | process.cwd(), 109 | '/suites/zora/test/', 110 | 'test' + i + '.js' 111 | ); 112 | const avaPath = join(process.cwd(), '/suites/ava/test/', 'test' + i + '.js'); 113 | const mochaPath = join( 114 | process.cwd(), 115 | '/suites/mocha/test/', 116 | 'test' + i + '.js' 117 | ); 118 | const tapePath = join( 119 | process.cwd(), 120 | '/suites/tape/test/', 121 | 'test' + i + '.js' 122 | ); 123 | const jestPath = join( 124 | process.cwd(), 125 | '/suites/jest/__tests__/', 126 | 'test' + i + '.js' 127 | ); 128 | const uvuPath = join(process.cwd(), '/suites/uvu/test', 'test' + i + '.js'); 129 | writeFileSync(zoraPath, zoraCode); 130 | writeFileSync(avaPath, avaCode); 131 | writeFileSync(mochaPath, mochaCode); 132 | writeFileSync(tapePath, tapeCode); 133 | writeFileSync(jestPath, jestCode); 134 | writeFileSync(uvuPath, uvuCode); 135 | writeFileSync(join(process.cwd(), '/suites/zora/index.js'), zoraIndex); 136 | } 137 | -------------------------------------------------------------------------------- /assert/readme.md: -------------------------------------------------------------------------------- 1 | # zora-assert 2 | 3 | Assertion library used by zora projects 4 | 5 | This library does not throw exception but returns [assertion results](#assertion-result) instead 6 | 7 | Note: this package is not published 8 | 9 | ## assertion API 10 | 11 | ```typescript 12 | export interface ComparatorAssertionFunction { 13 | (actual: unknown, expected: T, description?: string): AssertionResult; 14 | } 15 | 16 | export interface BooleanAssertionFunction { 17 | (actual: unknown, description?: string): AssertionResult; 18 | } 19 | 20 | export interface ErrorAssertionFunction { 21 | ( 22 | fn: Function, 23 | expected: RegExp | Function, 24 | description?: string 25 | ): IAssertionResult; 26 | (fn: Function, description?: string): IAssertionResult; 27 | } 28 | 29 | export interface MessageAssertionFunction { 30 | (message?: string): AssertionResult; 31 | } 32 | 33 | interface Assert { 34 | equal: ComparatorAssertionFunction; 35 | 36 | // alias 37 | equals: ComparatorAssertionFunction; 38 | 39 | // alias 40 | eq: ComparatorAssertionFunction; 41 | 42 | // alias 43 | same: ComparatorAssertionFunction; 44 | 45 | // alias 46 | deepEqual: ComparatorAssertionFunction; 47 | 48 | notEqual: ComparatorAssertionFunction; 49 | 50 | // alias 51 | notEquals: ComparatorAssertionFunction; 52 | 53 | // alias 54 | notEq: ComparatorAssertionFunction; 55 | 56 | // alias 57 | notDeepEqual: ComparatorAssertionFunction; 58 | 59 | // alias 60 | notSame: ComparatorAssertionFunction; 61 | 62 | is: ComparatorAssertionFunction; 63 | 64 | isNot: ComparatorAssertionFunction; 65 | 66 | ok: BooleanAssertionFunction; 67 | 68 | // alias 69 | truthy: BooleanAssertionFunction; 70 | 71 | notOk: BooleanAssertionFunction; 72 | 73 | // alias 74 | falsy: BooleanAssertionFunction; 75 | 76 | fail: MessageAssertionFunction; 77 | 78 | throws: ErrorAssertionFunction; 79 | } 80 | ``` 81 | 82 | ## assertion result 83 | 84 | ```typescript 85 | interface AssertionResult { 86 | pass: boolean; 87 | actual: unknown; 88 | expected: T; 89 | description: string; 90 | operator: Operator; 91 | at?: string; 92 | } 93 | ``` 94 | 95 | the ``at`` property is only there if the result has ``pass`` property set at ``false`` 96 | 97 | ## onResult 98 | 99 | You can pass a ``onResult`` callback function which the instance will call every time it produces a new assertion 100 | result 101 | 102 | ```Javascript 103 | import createAssert from 'zora-assert'; 104 | 105 | const assertions = []; 106 | const onResult = (item) => assertions.push(item); 107 | 108 | const assert = createAssert({onResult}); 109 | 110 | assert.ok('truthy'); 111 | assert.eq('foo', 'bar', 'some message'); 112 | 113 | /* 114 | assertions > 115 | [ 116 | { 117 | pass: true, 118 | actual: 'truthy', 119 | expected: 'truthy value', 120 | description: 'should be truthy', 121 | operator: 'ok' 122 | }, 123 | { 124 | pass: false, 125 | actual: 'foo', 126 | expected: 'bar', 127 | description: 'some message', 128 | operator: 'equal', 129 | at: 'relevant/stacktrace/line' 130 | } 131 | ] 132 | */ 133 | ``` 134 | 135 | ## Assert prototype 136 | 137 | The assert methods are bound to the instance, and you can destructure the instance to use the functions as if they were 138 | regular functions 139 | 140 | ```javascript 141 | import createAssert from 'zora-assert'; 142 | 143 | // destructure 144 | const {ok, eq} = createAssert({collect}); 145 | 146 | ok('truthy'); 147 | eq('foo', 'bar', 'some message'); 148 | ``` 149 | 150 | However, behind the scene, we use object delegation through the prototype chain so that you can enhance the Assert API 151 | globally if you wish to. 152 | 153 | ```javascript 154 | import createAssert, {Assert} from 'zora-assert'; 155 | 156 | // upgrade API 157 | Assert.isFoo = (expected, description = 'should be foo') => { 158 | return { 159 | pass: expected === 'foo', 160 | expected, 161 | actual: 'foo', 162 | operator: 'isFoo', 163 | description 164 | }; 165 | }; 166 | 167 | // then use regularly 168 | const {isFoo} = createAssert(); 169 | 170 | isFoo('bar') // failure 171 | isFoo('foo') // success 172 | ``` 173 | -------------------------------------------------------------------------------- /pta/test/samples/index.js: -------------------------------------------------------------------------------- 1 | import * as process from 'node:process'; 2 | import { execFile } from 'node:child_process'; 3 | import { promisify } from 'node:util'; 4 | import { readFileSync } from 'node:fs'; 5 | import { relative, resolve } from 'node:path'; 6 | 7 | import { test } from 'zora'; 8 | 9 | const exec = promisify(execFile); 10 | const node = process.execPath; 11 | 12 | const run = ({ args, cwd = process.cwd() }) => { 13 | const fullCWD = resolve(process.cwd(), cwd); 14 | const bin = relative(cwd, resolve(process.cwd(), 'src/bin.js')); 15 | return exec(node, [bin, ...args], { 16 | cwd: fullCWD, 17 | }); 18 | }; 19 | 20 | const loadFileContent = (path) => 21 | readFileSync(resolve(process.cwd(), path)).toString(); 22 | 23 | test(`only mode should only run test with only flag`, async (t) => { 24 | try { 25 | const { stderr, stdout } = await run({ 26 | args: ['*.js', '--only'], 27 | cwd: './test/samples/only/', 28 | }); 29 | t.ok(stdout); 30 | t.notOk(stderr); 31 | } catch (e) { 32 | t.fail(`should not have any error`); 33 | } 34 | }); 35 | 36 | test(`no only mode should throw when testing program uses "only"`, async (t) => { 37 | try { 38 | await run({ 39 | args: ['*.js'], 40 | cwd: 'test/samples/only/', 41 | }); 42 | t.fail('should not pass'); 43 | } catch (e) { 44 | t.eq(e.code, 1, 'exit code should be 1'); 45 | t.ok( 46 | e.stderr.includes( 47 | `Error: Can not use "only" method when not in "run only" mode` 48 | ) 49 | ); 50 | } 51 | }); 52 | 53 | test(`errored test suite should exit the process with code 1`, async (t) => { 54 | try { 55 | await run({ 56 | args: ['*.js'], 57 | cwd: 'test/samples/errored/', 58 | }); 59 | t.fail('should not pass'); 60 | } catch (e) { 61 | t.eq(e.code, 1, 'exit code should be 1'); 62 | t.ok(e.stderr.includes(`Error: some error`)); 63 | } 64 | }); 65 | 66 | test(`failing test suite should exit the process with code 1`, async (t) => { 67 | try { 68 | await run({ 69 | args: ['*.js'], 70 | cwd: 'test/samples/failing', 71 | }); 72 | t.fail('should not pass'); 73 | } catch (e) { 74 | t.eq(e.code, 1, 'exit code should be 1'); 75 | } 76 | }); 77 | 78 | test('--help should output the help content', async (t) => { 79 | try { 80 | const { stderr, stdout } = await run({ args: ['--help'] }); 81 | t.eq(stdout, loadFileContent('./src/usage.txt').toString()); 82 | t.notOk(stderr); 83 | } catch (e) { 84 | t.fail(`should not have any error`); 85 | } 86 | }); 87 | 88 | test('use tap reporter', async (t) => { 89 | try { 90 | const { stderr, stdout } = await run({ 91 | args: ['*.js', '-R', 'tap'], 92 | cwd: 'test/samples/dummy', 93 | }); 94 | t.eq(stdout, loadFileContent('test/samples/dummy/tap.txt')); 95 | t.notOk(stderr); 96 | } catch (e) { 97 | t.fail(`should not have any error`); 98 | } 99 | }); 100 | 101 | test('use log reporter', async (t) => { 102 | try { 103 | const { stderr, stdout } = await run({ 104 | args: ['*.js', '-R', 'log'], 105 | cwd: 'test/samples/dummy/', 106 | }); 107 | t.eq( 108 | stdout.replace(/"executionTime":\d+/g, '"executionTime":{TIME}'), 109 | loadFileContent('test/samples/dummy/log.txt') 110 | ); 111 | t.notOk(stderr); 112 | } catch (e) { 113 | t.fail(`should not have any error`); 114 | } 115 | }); 116 | 117 | test(`should load cjs files`, async (t) => { 118 | try { 119 | const { stderr, stdout } = await run({ 120 | args: ['test/*.{js,cjs}'], 121 | cwd: './test/samples/cjs', 122 | }); 123 | t.ok(stdout); 124 | t.notOk(stderr); 125 | } catch (e) { 126 | console.log(e); 127 | t.fail(`should not have any error`); 128 | } 129 | }); 130 | 131 | test(`should run test suite with es modules and import syntax`, async (t) => { 132 | try { 133 | const { stderr, stdout } = await run({ 134 | args: ['test/*.{js,mjs}'], 135 | cwd: './test/samples/es', 136 | }); 137 | t.ok(stdout); 138 | t.notOk(stderr); 139 | } catch (e) { 140 | console.log(e); 141 | t.fail(`should not have any error`); 142 | } 143 | }); 144 | 145 | test('should run test serially when there are top level await', async (t) => { 146 | try { 147 | const { stderr, stdout } = await run({ 148 | args: ['*.spec.js'], 149 | cwd: './test/samples/serial', 150 | }); 151 | t.ok(stdout); 152 | t.notOk(stderr); 153 | } catch (e) { 154 | console.log(e); 155 | t.fail(`should not have any error`); 156 | } 157 | }); 158 | -------------------------------------------------------------------------------- /reporters/src/diff/diagnostic/equal.js: -------------------------------------------------------------------------------- 1 | import { diffChars, diffJson } from 'diff'; 2 | import { leftPad, typeAsString } from '../utils.js'; 3 | import { EOL } from 'os'; 4 | import { compose } from '../../utils.js'; 5 | 6 | const actualParts = ({ added }) => added !== true; 7 | const expectedParts = ({ removed }) => removed !== true; 8 | 9 | const mapActualParts = 10 | (theme) => 11 | ({ value, removed }) => 12 | removed ? theme.diffActual(value) : value; 13 | 14 | const mapExpectedParts = 15 | (theme) => 16 | ({ value, added }) => 17 | added ? theme.diffExpected(value) : value; 18 | 19 | export const getDiffCharThemedMessage = 20 | (theme) => 21 | ({ actual, expected }) => { 22 | const diffs = diffChars(actual, expected); 23 | return { 24 | actual: diffs.filter(actualParts).map(mapActualParts(theme)).join(''), 25 | expected: diffs 26 | .filter(expectedParts) 27 | .map(mapExpectedParts(theme)) 28 | .join(''), 29 | }; 30 | }; 31 | 32 | const diffStrings = (theme) => { 33 | const diffChars = getDiffCharThemedMessage(theme); 34 | return ({ expected, actual }) => { 35 | const { expected: expectedMessage, actual: actualMessage } = diffChars({ 36 | expected, 37 | actual, 38 | }); 39 | 40 | return `diff in strings: 41 | ${theme.errorBadge('- actual')} ${theme.successBadge('+ expected')} 42 | 43 | ${theme.errorBadge('-')} ${actualMessage} 44 | ${theme.successBadge('+')} ${expectedMessage}`; 45 | }; 46 | }; 47 | 48 | const diffNumbers = 49 | (theme) => 50 | ({ expected, actual }) => 51 | `expected number to be ${theme.successBadge( 52 | expected 53 | )} but got ${theme.errorBadge(actual)}`; 54 | 55 | const diffBigInts = 56 | (theme) => 57 | ({ expected, actual }) => 58 | `expected bigint to be ${theme.successBadge( 59 | expected 60 | )} but got ${theme.errorBadge(actual)}`; 61 | 62 | const diffDates = (theme) => { 63 | const diffChars = getDiffCharThemedMessage(theme); 64 | return ({ expected, actual }) => { 65 | const { expected: expectedMessage, actual: actualMessage } = diffChars({ 66 | expected: expected.toISOString(), 67 | actual: actual.toISOString(), 68 | }); 69 | 70 | return `diff in dates: 71 | ${theme.errorBadge('- actual')} ${theme.successBadge('+ expected')} 72 | 73 | ${theme.errorBadge('-')} ${actualMessage} 74 | ${theme.successBadge('+')} ${expectedMessage}`; 75 | }; 76 | }; 77 | 78 | const diffBooleans = 79 | (theme) => 80 | ({ expected, actual }) => 81 | `expected boolean to be ${theme.emphasis( 82 | expected 83 | )} but got ${theme.emphasis(actual)}`; 84 | 85 | export const expandNewLines = (lines) => 86 | lines.flatMap((line) => { 87 | const { value, ...rest } = line; 88 | return value 89 | .split(EOL) 90 | .filter(Boolean) 91 | .map((newValue) => ({ 92 | ...rest, 93 | value: newValue, 94 | })); 95 | }); 96 | 97 | export const diffLine = (theme) => (diff) => { 98 | if (diff.added) { 99 | return `${theme.successBadge('+')} ${diff.value}`; 100 | } 101 | 102 | if (diff.removed) { 103 | return `${theme.errorBadge('-')} ${diff.value}`; 104 | } 105 | 106 | return leftPad(3, theme.disable(diff.value)); 107 | }; 108 | 109 | const getDiffJSONThemedMessage = (theme) => { 110 | const getLineDiff = diffLine(theme); 111 | return ({ actual, expected }) => { 112 | const diff = diffJson(actual, expected); 113 | return expandNewLines(diff).map(getLineDiff).map(leftPad(2)).join(EOL); 114 | }; 115 | }; 116 | 117 | const diffObjects = (theme) => { 118 | const diffJSON = getDiffJSONThemedMessage(theme); 119 | return ({ expected, actual }) => `diff in objects: 120 | ${theme.errorBadge('- actual')} ${theme.successBadge('+ expected')} 121 | 122 | ${diffJSON({ actual, expected })}`; 123 | }; 124 | 125 | const getDifferentTypeMessage = (theme) => { 126 | const printType = compose([theme.emphasis, typeAsString]); 127 | 128 | return ({ actual, expected }) => 129 | `expected a ${printType(expected)} but got a ${printType(actual)}`; 130 | }; 131 | 132 | export default (theme) => { 133 | const differentTypes = getDifferentTypeMessage(theme); 134 | 135 | const sameTypeDiff = { 136 | ['number']: diffNumbers(theme), 137 | ['bigint']: diffBigInts(theme), 138 | ['string']: diffStrings(theme), 139 | ['boolean']: diffBooleans(theme), 140 | ['object']: ({ expected, actual }) => { 141 | if (expected.constructor === Date) { 142 | return diffDates(theme)({ expected, actual }); 143 | } 144 | return diffObjects(theme)({ actual, expected }); 145 | }, 146 | }; 147 | 148 | return (diag) => { 149 | const { actual, expected } = diag; 150 | const expectedType = typeof expected; 151 | 152 | if (typeof actual !== expectedType) { 153 | return differentTypes({ actual, expected }); 154 | } 155 | 156 | return ( 157 | sameTypeDiff[expectedType]?.(diag) ?? 158 | `unsupported type ${theme.emphasis(expectedType)}` 159 | ); 160 | }; 161 | }; 162 | -------------------------------------------------------------------------------- /reporters/src/diff/writer/diagnostic.test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { Operator } from '../../../../assert/src/utils.js'; 3 | import getDiagnosticMessage from './diagnostic.js'; 4 | import getEqualDiagnosticMessage from '../diagnostic/equal.js'; 5 | 6 | const theme = new Proxy( 7 | {}, 8 | { 9 | get: function (target, prop, receiver) { 10 | return (val) => `<${prop}>${val}`; 11 | }, 12 | } 13 | ); 14 | 15 | const getMessage = getDiagnosticMessage({ theme }); 16 | 17 | test(`diagnostic messages`, (t) => { 18 | t.test(`equal diagnostic message`, (t) => { 19 | const getMessage = getEqualDiagnosticMessage(theme); 20 | 21 | t.test(`expected and actual have different types`, (t) => { 22 | t.eq( 23 | getMessage({ 24 | actual: undefined, 25 | expected: { some: 'value' }, 26 | operator: Operator.EQUAL, 27 | }), 28 | `expected a Object but got a undefined` 29 | ); 30 | }); 31 | 32 | t.test(`expected and actual are strings`, (t) => { 33 | t.eq( 34 | getMessage({ 35 | actual: 'fob', 36 | expected: 'foo', 37 | operator: Operator.EQUAL, 38 | }), 39 | `diff in strings: 40 | - actual + expected 41 | 42 | - fob 43 | + foo` 44 | ); 45 | }); 46 | 47 | t.test(`expected and actual are booleans`, (t) => { 48 | t.eq( 49 | getMessage({ actual: true, expected: false }), 50 | `expected boolean to be false but got true` 51 | ); 52 | }); 53 | 54 | t.test(`expected and actual are Dates`, (t) => { 55 | t.eq( 56 | getMessage({ 57 | actual: new Date(Date.UTC(2021, 4, 1)), 58 | expected: new Date(Date.UTC(2021, 5, 1)), 59 | }), 60 | `diff in dates: 61 | - actual + expected 62 | 63 | - 2021-05-01T00:00:00.000Z 64 | + 2021-06-01T00:00:00.000Z` 65 | ); 66 | }); 67 | 68 | t.test(`expected and actual are objects`, (t) => { 69 | const expected = { 70 | foo: 'bar', 71 | nested: { 72 | answer: 42, 73 | }, 74 | }; 75 | const actual = { 76 | foo: 'baz', 77 | nested: { 78 | answer: 43, 79 | }, 80 | }; 81 | t.eq( 82 | getMessage({ expected, actual }), 83 | `diff in objects: 84 | - actual + expected 85 | 86 | { 87 | - "foo": "baz", 88 | + "foo": "bar", 89 | "nested": { 90 | - "answer": 43 91 | + "answer": 42 92 | } 93 | }` 94 | ); 95 | }); 96 | 97 | t.test(`expected and actual are numbers`, (t) => { 98 | const expected = 5; 99 | const actual = 3; 100 | t.eq( 101 | getMessage({ expected, actual }), 102 | `expected number to be 5 but got 3` 103 | ); 104 | }); 105 | 106 | t.test(`expected and actual are BigInt's`, (t) => { 107 | const expected = 5n; 108 | const actual = 3n; 109 | t.eq( 110 | getMessage({ expected, actual }), 111 | `expected bigint to be 5 but got 3` 112 | ); 113 | }); 114 | }); 115 | 116 | t.test(`ok diagnostic message`, (t) => { 117 | t.eq( 118 | getMessage({ actual: null, operator: Operator.OK }), 119 | `expected "truthy" but got null` 120 | ); 121 | 122 | t.eq( 123 | getMessage({ actual: '', operator: Operator.OK }), 124 | `expected "truthy" but got ""`, 125 | 'display double quotes when actual is an empty string' 126 | ); 127 | }); 128 | 129 | t.test(`notOk diagnostic message`, (t) => { 130 | t.eq( 131 | getMessage({ actual: 'foo', operator: Operator.NOT_OK }), 132 | `expected "falsy" but got "foo"` 133 | ); 134 | 135 | t.eq( 136 | getMessage({ actual: {}, operator: Operator.NOT_OK }), 137 | `expected "falsy" but got {}` 138 | ); 139 | 140 | t.eq( 141 | getMessage({ actual: [], operator: Operator.NOT_OK }), 142 | `expected "falsy" but got []` 143 | ); 144 | }); 145 | 146 | t.test(`fail diagnostic message`, (t) => { 147 | t.eq( 148 | getMessage({ 149 | description: 'should not get here', 150 | operator: Operator.FAIL, 151 | }), 152 | `expected fail not to be called, but was called as "should not get here"` 153 | ); 154 | }); 155 | 156 | t.test(`notEqual diagnostic message`, (t) => { 157 | t.eq( 158 | getMessage({ operator: Operator.NOT_EQUAL }), 159 | `expected the arguments not to be equivalent but they were` 160 | ); 161 | }); 162 | 163 | t.test(`is diagnostic message`, (t) => { 164 | t.eq( 165 | getMessage({ operator: Operator.IS }), 166 | `expected references to be the same but they were not` 167 | ); 168 | }); 169 | 170 | t.test(`isNot diagnostic message`, (t) => { 171 | t.eq( 172 | getMessage({ operator: Operator.IS_NOT }), 173 | `expected references not to be the same but they were` 174 | ); 175 | }); 176 | 177 | t.test(`unknown operator diagnostic message`, (t) => { 178 | t.eq( 179 | getMessage({ operator: 'wooty' }), 180 | `unknown operator wooty` 181 | ); 182 | }); 183 | 184 | t.test(`throws diagnostic message`, (t) => { 185 | t.test(`did not throw`, (t) => { 186 | t.eq( 187 | getMessage({ operator: Operator.THROWS }), 188 | `expected to throw but it did not` 189 | ); 190 | }); 191 | 192 | t.test('throws not matching error', (t) => { 193 | t.eq( 194 | getMessage({ 195 | operator: Operator.THROWS, 196 | actual: 'foo', 197 | expected: /bar/, 198 | }), 199 | `expected the error thrown to match /bar/ but it matched foo` 200 | ); 201 | }); 202 | }); 203 | 204 | t.test(`equal diagnostic message`, (t) => { 205 | const getMessage = getEqualDiagnosticMessage(theme); 206 | t.test(`expected and actual have different types`, (t) => { 207 | t.eq( 208 | getMessage({ 209 | actual: undefined, 210 | expected: { some: 'value' }, 211 | operator: Operator.EQUAL, 212 | }), 213 | `expected a Object but got a undefined` 214 | ); 215 | }); 216 | }); 217 | 218 | t.test(`timeout diagnostic`, (t) => { 219 | t.eq( 220 | getMessage({ operator: 'timeout', actual: 'timeout description' }), 221 | `timeout description` 222 | ); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /zora/readme.md: -------------------------------------------------------------------------------- 1 | # Zora 2 | 3 | [![npm](https://badgen.net/npm/v/zora)](https://www.npmjs.com/package/zora) 4 | [![install size](https://badgen.net/packagephobia/install/zora)](https://packagephobia.now.sh/result?p=zora) 5 | 6 | ## Usage 7 | 8 | The library is a _regular_ Javascript module and can be directly imported from a CDN: 9 | 10 | ```Javascript 11 | import {test} from 'https://unpkg.com/zora@latest/dist/index.js' 12 | 13 | test(`hello from zora`, ({ok}) => { 14 | ok(true, 'it worked'); 15 | }) 16 | ``` 17 | 18 | (Like in the [following codepen](https://codepen.io/lorenzofox3/pen/LYWOaYV?editors=1111)) 19 | 20 | Or installed via a package manager such [NPM](https://www.npmjs.com/) by running the command (assuming you have [Nodejs](https://nodejs.org/en/) installed on your machine): 21 | 22 | ``npm i -D zora`` 23 | 24 | You can then build your testing program by using the exported ``test`` function 25 | 26 | ```Javascript 27 | import {test} from 'zora'; 28 | 29 | test(`my very first test`, (assertion) => { 30 | const input = false; 31 | assertion.ok(input, 'input should be truthy'); 32 | }) 33 | 34 | ``` 35 | 36 | You can run the testing program (with node's runtime for example) and it will start reporting its execution into the console 37 | 38 |
39 | test-output.tap 40 | 41 | ```TAP 42 | TAP version 13 43 | # my very first test 44 | not ok 1 - input should be truthy 45 | --- 46 | actual: false 47 | expected: "truthy value" 48 | operator: "ok" 49 | at: " file:///path/to/sample.js:5:13" 50 | ... 51 | 52 | 1..1 53 | # tests 1 54 | # pass 0 55 | # fail 1 56 | # skip 0 57 | 58 | ``` 59 | 60 |
61 | 62 | ## Reporters 63 | 64 | This output format is called [TAP](https://testanything.org/tap-version-13-specification.html) (Test Anything Protocol). It is a standard text based format a machine can easily parse. It means there are [plenty of reporters](https://www.npmjs.com/search?q=tap%20reporter) you can pipe the standard output stream into (not only in the Javascript world). This will help you to tailor the reporting for your particular needs. 65 | 66 | for example, you can use [tap-diff](https://www.npmjs.com/package/tap-diff): 67 | ``node path/to/testing/program.js | tap-diff`` 68 | 69 | You can even use basic bash command: 70 | 71 | ``node path/to/testing/program.js | grep '^not ok\|^\s'`` will output a basic, straight to the point test report: 72 | 73 | ``` 74 | not ok 1 - input should be truthy 75 | --- 76 | actual: false 77 | expected: "truthy value" 78 | operator: "ok" 79 | at: " file:///path/to/sample.js:5:13" 80 | ... 81 | ``` 82 | 83 | That is the beauty of using different processes to run the testing program and to format its output: it remains very flexible. 84 | 85 | ## Assertion API 86 | 87 | When you start a test suite with the ``test`` function. The spec functions you pass as argument will get an instance of the [Assertion object](../assert) so you can write a wide range of different expectations. 88 | 89 | For the best performances, all the spec functions run concurrently unless you specifically wait for them within an asynchronous function (if for some reason, you want to run some test one after the other, in a serial mode). 90 | 91 |
92 | control-flow.js 93 | 94 | ```Javascript 95 | import {test} from 'zora'; 96 | 97 | let state = 0; 98 | 99 | test('test 1', t => { 100 | t.ok(true); 101 | state++; 102 | }); 103 | 104 | test('test 2', t => { 105 | //Maybe yes maybe no, you have no guarantee ! In this case it will work as everything is sync 106 | t.equal(state, 1); 107 | }); 108 | 109 | //Same thing here even in nested tests 110 | test('grouped', t => { 111 | let state = 0; 112 | 113 | t.test('test 1', t => { 114 | t.ok(true); 115 | state++; 116 | }); 117 | 118 | t.test('test 2', t => { 119 | //Maybe yes maybe no, you have no guarantee ! In this case it will work as everything is sync 120 | t.equal(state, 1); 121 | }); 122 | }); 123 | 124 | //And 125 | test('grouped', t=>{ 126 | let state = 0; 127 | 128 | t.test('test 1', async t=>{ 129 | t.ok(true); 130 | await wait(100); 131 | state++; 132 | }); 133 | 134 | test('test 2', t=>{ 135 | t.equal(state, 0, 'see the old state value as it will have started to run before test 1 is done'); 136 | }); 137 | }); 138 | 139 | //But 140 | test('grouped', async t => { 141 | let state = 0; 142 | 143 | //specifically WAIT for the end of this test before moving on ! 144 | await t.test('test 1', async t => { 145 | t.ok(true); 146 | await wait(100); 147 | state++; 148 | }); 149 | 150 | test('test 2', t => { 151 | t.equal(state, 1, 'see the updated value!'); 152 | }); 153 | }); 154 | ``` 155 | 156 |
157 | 158 | 159 | ## Environment variables 160 | 161 | You can _configure_ the testing program with environment variables. With nodejs, simply pass it with the command line: 162 | 163 | ``ZORA_REPORTER=json node path/to/testing/program.js`` 164 | 165 | In the browser, you have to set it as a global before the testing program runs: 166 | 167 | ```HTML 168 | 171 | 178 | ``` 179 | 180 | ### ZORA_REPORTER=json 181 | 182 | By default, the output is a TAP stream; but you can decide to produce a stream defined by [the zora json protocol messages](../reporters) if you which to build a custom reporter on top of it. 183 | 184 | ### ZORA_ONLY=true 185 | 186 | Beside the ``test`` function you can use the ``only`` function if you wish to skip all the other tests. However, from the testing program perspective, ``only`` is meaningless: it is just a convenience for a developer, locally on its machine. 187 | So if you wish to use ``only`` you need to pass the ``ZORA_ONLY`` environment variable, otherwise the program will throw an exception to prevent ``only`` statement to slip in the remote versioning control system. 188 | 189 | Beware that if you want to run only a nested test suite, all the parent test suites must use the ``only statement`:` 190 | 191 |
192 | test-with-only.js 193 | 194 | ```Javascript 195 | import {only, test} from 'zora'; 196 | 197 | test('will be skipped', t => { 198 | t.ok(false); 199 | }) 200 | 201 | only('some test', t => { 202 | 203 | // will be skipped as well 204 | t.test('some nested test', t => { 205 | t.ok(false); 206 | }); 207 | 208 | // will run 209 | t.only('some other nested test', t => { 210 | t.ok(true); 211 | }); 212 | }); 213 | ``` 214 | 215 |
216 | 217 | ### ZORA_TIMEOUT 218 | 219 | In milliseconds: change the default test timeout value (see below) 220 | 221 | ## test timeout 222 | 223 | If a test hangs or takes too long to complete, it will report a failure. By default, the threshold is 5000ms. You can change that global value thanks to the ``ZORA_TIMEOUT`` environment variable. 224 | You can change that value for a given text as well, thanks to the options object: 225 | 226 |
227 | timeout example 228 | 229 | ```Javascript 230 | test( 231 | 'broken promise', 232 | ({ ok }) => { 233 | return new Promise(() => {}).then(() => { 234 | ok(true); 235 | }); 236 | }, 237 | { timeout: 500 } 238 | ); 239 | ``` 240 | 241 |
242 | 243 | ## skip a test 244 | 245 | You can skip a test using the root level ``skip`` function or within a test suite using the ``skip`` method of the assertion object 246 | 247 |
248 | test-with-skip.js 249 | 250 | ```Javascript 251 | import {skip, test} from 'zora'; 252 | 253 | skip('will be skipped', t => { 254 | t.ok(false); 255 | }) 256 | 257 | test('some test', t => { 258 | 259 | // will be skipped as well 260 | t.skip('some nested test', t => { 261 | t.ok(false); 262 | }); 263 | 264 | // will run 265 | t.test('some other nested test', t => { 266 | t.ok(true); 267 | }); 268 | }); 269 | ``` 270 | 271 |
272 | -------------------------------------------------------------------------------- /assert/test/assert.js: -------------------------------------------------------------------------------- 1 | import { test } from 'zora'; 2 | import { 3 | equal, 4 | fail, 5 | is, 6 | isNot, 7 | notEqual, 8 | notOk, 9 | ok, 10 | throws, 11 | } from '../src/assert.js'; 12 | 13 | test('"equal" operator', ({ eq }) => { 14 | eq( 15 | equal('foo', 'foo', 'a description'), 16 | { 17 | pass: true, 18 | actual: 'foo', 19 | expected: 'foo', 20 | description: 'a description', 21 | operator: 'equal', 22 | }, 23 | 'should return a passing assert result when both string literals are equal' 24 | ); 25 | 26 | eq( 27 | equal(4, 4, 'a description'), 28 | { 29 | pass: true, 30 | actual: 4, 31 | expected: 4, 32 | description: 'a description', 33 | operator: 'equal', 34 | }, 35 | 'should return a passing assert result when both number literals are equal' 36 | ); 37 | 38 | eq( 39 | equal(true, true, 'a description'), 40 | { 41 | pass: true, 42 | actual: true, 43 | expected: true, 44 | description: 'a description', 45 | operator: 'equal', 46 | }, 47 | 'should return a passing assert result when both boolean literals are equal' 48 | ); 49 | 50 | eq( 51 | equal({ prop: 'val' }, { prop: 'val' }, 'a description'), 52 | { 53 | pass: true, 54 | actual: { prop: 'val' }, 55 | expected: { prop: 'val' }, 56 | description: 'a description', 57 | operator: 'equal', 58 | }, 59 | 'should return a passing assert result when two object are deeply equal' 60 | ); 61 | 62 | eq( 63 | equal('foo', 'bar'), 64 | { 65 | pass: false, 66 | actual: 'foo', 67 | expected: 'bar', 68 | description: 'should be equivalent', 69 | operator: 'equal', 70 | }, 71 | 'should return a failing assert result when two string literals are different' 72 | ); 73 | 74 | eq( 75 | equal(4, 5, 'a description'), 76 | { 77 | pass: false, 78 | actual: 4, 79 | expected: 5, 80 | description: 'a description', 81 | operator: 'equal', 82 | }, 83 | 'should return a failing assert result when two number literals are different' 84 | ); 85 | 86 | eq( 87 | equal(true, false, 'a description'), 88 | { 89 | pass: false, 90 | actual: true, 91 | expected: false, 92 | description: 'a description', 93 | operator: 'equal', 94 | }, 95 | 'should return a failing assert result when two boolean literals are different' 96 | ); 97 | 98 | eq( 99 | equal({ prop: 'val' }, { prop: 'val' }, 'a description'), 100 | { 101 | pass: true, 102 | actual: { prop: 'val' }, 103 | expected: { prop: 'val' }, 104 | description: 'a description', 105 | operator: 'equal', 106 | }, 107 | 'should return a passing assert result when two object are deeply equal' 108 | ); 109 | 110 | eq( 111 | equal(new Map(), new Map([['a', 123]]), 'a description'), 112 | { 113 | pass: false, 114 | actual: new Map(), 115 | expected: new Map([['a', 123]]), 116 | description: 'a description', 117 | operator: 'equal', 118 | }, 119 | 'should return a failing assert result when two Maps are different' 120 | ); 121 | 122 | eq( 123 | equal( 124 | { map: new Map([['a', 123]]) }, 125 | { map: new Map([['a', 123]]) }, 126 | 'a description' 127 | ), 128 | { 129 | pass: true, 130 | actual: { map: new Map([['a', 123]]) }, 131 | expected: { map: new Map([['a', 123]]) }, 132 | description: 'a description', 133 | operator: 'equal', 134 | }, 135 | 'should return a passing assert result when two nested Maps are the same' 136 | ); 137 | 138 | eq( 139 | equal(new Set(), new Set(['a', 'b']), 'a description'), 140 | { 141 | pass: false, 142 | actual: new Set(), 143 | expected: new Set(['a', 'b']), 144 | description: 'a description', 145 | operator: 'equal', 146 | }, 147 | 'should return a failing assert result when two Sets are different' 148 | ); 149 | 150 | eq( 151 | equal( 152 | { set: new Set(['a', 'b']) }, 153 | { set: new Set(['a', 'b']) }, 154 | 'a description' 155 | ), 156 | { 157 | pass: true, 158 | actual: { set: new Set(['a', 'b']) }, 159 | expected: { set: new Set(['a', 'b']) }, 160 | description: 'a description', 161 | operator: 'equal', 162 | }, 163 | 'should return a passing assert result when two nested Sets are the same' 164 | ); 165 | }); 166 | 167 | test('"not equal" operator', ({ eq }) => { 168 | eq( 169 | notEqual('foo', 'bar'), 170 | { 171 | pass: true, 172 | actual: 'foo', 173 | expected: 'bar', 174 | description: 'should not be equivalent', 175 | operator: 'notEqual', 176 | }, 177 | 'should return a passing assert result' 178 | ); 179 | 180 | eq( 181 | notEqual('foo', 'foo'), 182 | { 183 | pass: false, 184 | actual: 'foo', 185 | expected: 'foo', 186 | description: 'should not be equivalent', 187 | operator: 'notEqual', 188 | }, 189 | 'should return a failing assert result' 190 | ); 191 | }); 192 | 193 | test('"is" operator', ({ eq }) => { 194 | const instance = {}; 195 | eq( 196 | is({ foo: 'bar' }, { foo: 'bar' }), 197 | { 198 | pass: false, 199 | actual: { foo: 'bar' }, 200 | expected: { foo: 'bar' }, 201 | description: 'should be the same', 202 | operator: 'is', 203 | }, 204 | 'should return a failing assert result when references are different' 205 | ); 206 | eq( 207 | is(instance, instance), 208 | { 209 | pass: true, 210 | actual: {}, 211 | expected: {}, 212 | description: 'should be the same', 213 | operator: 'is', 214 | }, 215 | 'should return a passing assert result when references are the same' 216 | ); 217 | }); 218 | 219 | test('"isNot" operator', ({ eq }) => { 220 | const instance = {}; 221 | eq( 222 | isNot({ foo: 'bar' }, { foo: 'bar' }), 223 | { 224 | pass: true, 225 | actual: { foo: 'bar' }, 226 | expected: { foo: 'bar' }, 227 | description: 'should not be the same', 228 | operator: 'isNot', 229 | }, 230 | 'should return a passing assert result when references are different' 231 | ); 232 | 233 | eq( 234 | isNot(instance, instance), 235 | { 236 | pass: false, 237 | actual: {}, 238 | expected: {}, 239 | description: 'should not be the same', 240 | operator: 'isNot', 241 | }, 242 | 'should return a failing assert result when references are the same' 243 | ); 244 | }); 245 | 246 | test('"ok" operator', ({ eq }) => { 247 | eq( 248 | ok('truthy'), 249 | { 250 | pass: true, 251 | actual: 'truthy', 252 | description: 'should be truthy', 253 | expected: 'truthy value', 254 | operator: 'ok', 255 | }, 256 | 'should return a passing assert result when actual value is truthy' 257 | ); 258 | 259 | eq( 260 | ok(null), 261 | { 262 | pass: false, 263 | actual: null, 264 | expected: 'truthy value', 265 | description: 'should be truthy', 266 | operator: 'ok', 267 | }, 268 | 'should return a failing assert result when actual value is not truthy' 269 | ); 270 | 271 | eq( 272 | ok(undefined), 273 | { 274 | pass: false, 275 | actual: undefined, 276 | expected: 'truthy value', 277 | description: 'should be truthy', 278 | operator: 'ok', 279 | }, 280 | 'should return a failing assert result when actual value is not truthy' 281 | ); 282 | 283 | eq( 284 | ok(''), 285 | { 286 | pass: false, 287 | actual: '', 288 | expected: 'truthy value', 289 | description: 'should be truthy', 290 | operator: 'ok', 291 | }, 292 | 'should return a failing assert result when actual value is not truthy' 293 | ); 294 | }); 295 | 296 | test('"notOk" operator', ({ eq }) => { 297 | eq( 298 | notOk('truthy'), 299 | { 300 | pass: false, 301 | actual: 'truthy', 302 | description: 'should be falsy', 303 | expected: 'falsy value', 304 | operator: 'notOk', 305 | }, 306 | 'should return a failing assert result when actual value is not falsy' 307 | ); 308 | 309 | eq( 310 | notOk(null), 311 | { 312 | pass: true, 313 | actual: null, 314 | expected: 'falsy value', 315 | description: 'should be falsy', 316 | operator: 'notOk', 317 | }, 318 | 'should return a passing assert result when actual value is falsy' 319 | ); 320 | }); 321 | 322 | test('"fail" operator', ({ eq }) => { 323 | eq( 324 | fail('this should fail'), 325 | { 326 | pass: false, 327 | actual: 'fail called', 328 | expected: 'fail not called', 329 | description: 'this should fail', 330 | operator: 'fail', 331 | }, 332 | 'should always return a failing assert result' 333 | ); 334 | }); 335 | 336 | test('throws', ({ eq }) => { 337 | const regexp = /^totally/i; 338 | 339 | class CustomError extends Error { 340 | constructor() { 341 | super('custom error'); 342 | } 343 | } 344 | 345 | eq( 346 | throws(() => { 347 | throw new Error('Totally expected error'); 348 | }, regexp), 349 | { 350 | pass: true, 351 | actual: 'Totally expected error', 352 | expected: '/^totally/i', 353 | description: 'should throw', 354 | operator: 'throws', 355 | }, 356 | 'expected is a regexp, passing' 357 | ); 358 | 359 | eq( 360 | throws(() => { 361 | throw new Error('not the expected error'); 362 | }, regexp), 363 | { 364 | pass: false, 365 | actual: 'not the expected error', 366 | expected: '/^totally/i', 367 | description: 'should throw', 368 | operator: 'throws', 369 | }, 370 | 'expected is a regexp, failing' 371 | ); 372 | 373 | eq( 374 | throws(() => { 375 | throw new CustomError(); 376 | }, CustomError), 377 | { 378 | pass: true, 379 | actual: CustomError, 380 | expected: CustomError, 381 | description: 'should throw', 382 | operator: 'throws', 383 | }, 384 | 'expected is a constructor, passing' 385 | ); 386 | 387 | eq( 388 | throws(() => { 389 | throw new Error(); 390 | }, CustomError), 391 | { 392 | pass: false, 393 | actual: Error, 394 | expected: CustomError, 395 | description: 'should throw', 396 | operator: 'throws', 397 | }, 398 | 'expected is a constructor, failing' 399 | ); 400 | 401 | eq( 402 | throws(() => { 403 | throw new Error('whatever'); 404 | }, 'custom description'), 405 | { 406 | pass: true, 407 | description: 'custom description', 408 | expected: 'any error thrown', 409 | actual: 'error thrown', 410 | operator: 'throws', 411 | } 412 | ); 413 | 414 | eq( 415 | throws(() => {}), 416 | { 417 | pass: false, 418 | description: 'should throw', 419 | expected: 'any error thrown', 420 | actual: 'no error thrown', 421 | operator: 'throws', 422 | } 423 | ); 424 | }); 425 | -------------------------------------------------------------------------------- /examples/browser-vanilla-vite/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-vanilla-vite", 3 | "version": "0.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "0.0.0", 9 | "dependencies": { 10 | "is-even": "~1.0.0" 11 | }, 12 | "devDependencies": { 13 | "@lorenzofox3/for-await": "^0.2.2", 14 | "playwright": "^1.22.2", 15 | "vite": "^2.9.18", 16 | "ws": "^8.8.0", 17 | "zora": "*" 18 | } 19 | }, 20 | "node_modules/@lorenzofox3/for-await": { 21 | "version": "0.2.2", 22 | "resolved": "https://registry.npmjs.org/@lorenzofox3/for-await/-/for-await-0.2.2.tgz", 23 | "integrity": "sha512-IKIjQAyl6yu/kWTCqpc4QQwkA048pgsvvwxb8bMoqC4NBdypVBGj0wxFTL882sw8WzMSpov3g/pP6RxPhqbJHA==", 24 | "dev": true, 25 | "engines": { 26 | "node": ">=9.3.0" 27 | } 28 | }, 29 | "node_modules/esbuild": { 30 | "version": "0.14.43", 31 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.43.tgz", 32 | "integrity": "sha512-Uf94+kQmy/5jsFwKWiQB4hfo/RkM9Dh7b79p8yqd1tshULdr25G2szLz631NoH3s2ujnKEKVD16RmOxvCNKRFA==", 33 | "dev": true, 34 | "hasInstallScript": true, 35 | "bin": { 36 | "esbuild": "bin/esbuild" 37 | }, 38 | "engines": { 39 | "node": ">=12" 40 | }, 41 | "optionalDependencies": { 42 | "esbuild-android-64": "0.14.43", 43 | "esbuild-android-arm64": "0.14.43", 44 | "esbuild-darwin-64": "0.14.43", 45 | "esbuild-darwin-arm64": "0.14.43", 46 | "esbuild-freebsd-64": "0.14.43", 47 | "esbuild-freebsd-arm64": "0.14.43", 48 | "esbuild-linux-32": "0.14.43", 49 | "esbuild-linux-64": "0.14.43", 50 | "esbuild-linux-arm": "0.14.43", 51 | "esbuild-linux-arm64": "0.14.43", 52 | "esbuild-linux-mips64le": "0.14.43", 53 | "esbuild-linux-ppc64le": "0.14.43", 54 | "esbuild-linux-riscv64": "0.14.43", 55 | "esbuild-linux-s390x": "0.14.43", 56 | "esbuild-netbsd-64": "0.14.43", 57 | "esbuild-openbsd-64": "0.14.43", 58 | "esbuild-sunos-64": "0.14.43", 59 | "esbuild-windows-32": "0.14.43", 60 | "esbuild-windows-64": "0.14.43", 61 | "esbuild-windows-arm64": "0.14.43" 62 | } 63 | }, 64 | "node_modules/esbuild-darwin-64": { 65 | "version": "0.14.43", 66 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.43.tgz", 67 | "integrity": "sha512-/3PSilx011ttoieRGkSZ0XV8zjBf2C9enV4ScMMbCT4dpx0mFhMOpFnCHkOK0pWGB8LklykFyHrWk2z6DENVUg==", 68 | "cpu": [ 69 | "x64" 70 | ], 71 | "dev": true, 72 | "optional": true, 73 | "os": [ 74 | "darwin" 75 | ], 76 | "engines": { 77 | "node": ">=12" 78 | } 79 | }, 80 | "node_modules/fsevents": { 81 | "version": "2.3.2", 82 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 83 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 84 | "dev": true, 85 | "hasInstallScript": true, 86 | "optional": true, 87 | "os": [ 88 | "darwin" 89 | ], 90 | "engines": { 91 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 92 | } 93 | }, 94 | "node_modules/function-bind": { 95 | "version": "1.1.1", 96 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 97 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 98 | "dev": true 99 | }, 100 | "node_modules/has": { 101 | "version": "1.0.3", 102 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 103 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 104 | "dev": true, 105 | "dependencies": { 106 | "function-bind": "^1.1.1" 107 | }, 108 | "engines": { 109 | "node": ">= 0.4.0" 110 | } 111 | }, 112 | "node_modules/is-buffer": { 113 | "version": "1.1.6", 114 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 115 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 116 | }, 117 | "node_modules/is-core-module": { 118 | "version": "2.9.0", 119 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", 120 | "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", 121 | "dev": true, 122 | "dependencies": { 123 | "has": "^1.0.3" 124 | }, 125 | "funding": { 126 | "url": "https://github.com/sponsors/ljharb" 127 | } 128 | }, 129 | "node_modules/is-even": { 130 | "version": "1.0.0", 131 | "resolved": "https://registry.npmjs.org/is-even/-/is-even-1.0.0.tgz", 132 | "integrity": "sha512-LEhnkAdJqic4Dbqn58A0y52IXoHWlsueqQkKfMfdEnIYG8A1sm/GHidKkS6yvXlMoRrkM34csHnXQtOqcb+Jzg==", 133 | "dependencies": { 134 | "is-odd": "^0.1.2" 135 | }, 136 | "engines": { 137 | "node": ">=0.10.0" 138 | } 139 | }, 140 | "node_modules/is-number": { 141 | "version": "3.0.0", 142 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", 143 | "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", 144 | "dependencies": { 145 | "kind-of": "^3.0.2" 146 | }, 147 | "engines": { 148 | "node": ">=0.10.0" 149 | } 150 | }, 151 | "node_modules/is-odd": { 152 | "version": "0.1.2", 153 | "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-0.1.2.tgz", 154 | "integrity": "sha512-Ri7C2K7o5IrUU9UEI8losXJCCD/UtsaIrkR5sxIcFg4xQ9cRJXlWA5DQvTE0yDc0krvSNLsRGXN11UPS6KyfBw==", 155 | "dependencies": { 156 | "is-number": "^3.0.0" 157 | }, 158 | "engines": { 159 | "node": ">=0.10.0" 160 | } 161 | }, 162 | "node_modules/kind-of": { 163 | "version": "3.2.2", 164 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 165 | "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", 166 | "dependencies": { 167 | "is-buffer": "^1.1.5" 168 | }, 169 | "engines": { 170 | "node": ">=0.10.0" 171 | } 172 | }, 173 | "node_modules/nanoid": { 174 | "version": "3.3.7", 175 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 176 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 177 | "dev": true, 178 | "funding": [ 179 | { 180 | "type": "github", 181 | "url": "https://github.com/sponsors/ai" 182 | } 183 | ], 184 | "bin": { 185 | "nanoid": "bin/nanoid.cjs" 186 | }, 187 | "engines": { 188 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 189 | } 190 | }, 191 | "node_modules/path-parse": { 192 | "version": "1.0.7", 193 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 194 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 195 | "dev": true 196 | }, 197 | "node_modules/picocolors": { 198 | "version": "1.0.0", 199 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 200 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", 201 | "dev": true 202 | }, 203 | "node_modules/playwright": { 204 | "version": "1.22.2", 205 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.22.2.tgz", 206 | "integrity": "sha512-hUTpg7LytIl3/O4t0AQJS1V6hWsaSY5uZ7w1oCC8r3a1AQN5d6otIdCkiB3cbzgQkcMaRxisinjMFMVqZkybdQ==", 207 | "dev": true, 208 | "hasInstallScript": true, 209 | "dependencies": { 210 | "playwright-core": "1.22.2" 211 | }, 212 | "bin": { 213 | "playwright": "cli.js" 214 | }, 215 | "engines": { 216 | "node": ">=14" 217 | } 218 | }, 219 | "node_modules/playwright-core": { 220 | "version": "1.22.2", 221 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.22.2.tgz", 222 | "integrity": "sha512-w/hc/Ld0RM4pmsNeE6aL/fPNWw8BWit2tg+TfqJ3+p59c6s3B6C8mXvXrIPmfQEobkcFDc+4KirNzOQ+uBSP1Q==", 223 | "dev": true, 224 | "bin": { 225 | "playwright": "cli.js" 226 | }, 227 | "engines": { 228 | "node": ">=14" 229 | } 230 | }, 231 | "node_modules/postcss": { 232 | "version": "8.4.32", 233 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", 234 | "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", 235 | "dev": true, 236 | "funding": [ 237 | { 238 | "type": "opencollective", 239 | "url": "https://opencollective.com/postcss/" 240 | }, 241 | { 242 | "type": "tidelift", 243 | "url": "https://tidelift.com/funding/github/npm/postcss" 244 | }, 245 | { 246 | "type": "github", 247 | "url": "https://github.com/sponsors/ai" 248 | } 249 | ], 250 | "dependencies": { 251 | "nanoid": "^3.3.7", 252 | "picocolors": "^1.0.0", 253 | "source-map-js": "^1.0.2" 254 | }, 255 | "engines": { 256 | "node": "^10 || ^12 || >=14" 257 | } 258 | }, 259 | "node_modules/resolve": { 260 | "version": "1.22.0", 261 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", 262 | "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", 263 | "dev": true, 264 | "dependencies": { 265 | "is-core-module": "^2.8.1", 266 | "path-parse": "^1.0.7", 267 | "supports-preserve-symlinks-flag": "^1.0.0" 268 | }, 269 | "bin": { 270 | "resolve": "bin/resolve" 271 | }, 272 | "funding": { 273 | "url": "https://github.com/sponsors/ljharb" 274 | } 275 | }, 276 | "node_modules/rollup": { 277 | "version": "2.75.6", 278 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.6.tgz", 279 | "integrity": "sha512-OEf0TgpC9vU6WGROJIk1JA3LR5vk/yvqlzxqdrE2CzzXnqKXNzbAwlWUXis8RS3ZPe7LAq+YUxsRa0l3r27MLA==", 280 | "dev": true, 281 | "bin": { 282 | "rollup": "dist/bin/rollup" 283 | }, 284 | "engines": { 285 | "node": ">=10.0.0" 286 | }, 287 | "optionalDependencies": { 288 | "fsevents": "~2.3.2" 289 | } 290 | }, 291 | "node_modules/source-map-js": { 292 | "version": "1.0.2", 293 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 294 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 295 | "dev": true, 296 | "engines": { 297 | "node": ">=0.10.0" 298 | } 299 | }, 300 | "node_modules/supports-preserve-symlinks-flag": { 301 | "version": "1.0.0", 302 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 303 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 304 | "dev": true, 305 | "engines": { 306 | "node": ">= 0.4" 307 | }, 308 | "funding": { 309 | "url": "https://github.com/sponsors/ljharb" 310 | } 311 | }, 312 | "node_modules/vite": { 313 | "version": "2.9.18", 314 | "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.18.tgz", 315 | "integrity": "sha512-sAOqI5wNM9QvSEE70W3UGMdT8cyEn0+PmJMTFvTB8wB0YbYUWw3gUbY62AOyrXosGieF2htmeLATvNxpv/zNyQ==", 316 | "dev": true, 317 | "dependencies": { 318 | "esbuild": "^0.14.27", 319 | "postcss": "^8.4.13", 320 | "resolve": "^1.22.0", 321 | "rollup": ">=2.59.0 <2.78.0" 322 | }, 323 | "bin": { 324 | "vite": "bin/vite.js" 325 | }, 326 | "engines": { 327 | "node": ">=12.2.0" 328 | }, 329 | "optionalDependencies": { 330 | "fsevents": "~2.3.2" 331 | }, 332 | "peerDependencies": { 333 | "less": "*", 334 | "sass": "*", 335 | "stylus": "*" 336 | }, 337 | "peerDependenciesMeta": { 338 | "less": { 339 | "optional": true 340 | }, 341 | "sass": { 342 | "optional": true 343 | }, 344 | "stylus": { 345 | "optional": true 346 | } 347 | } 348 | }, 349 | "node_modules/ws": { 350 | "version": "8.8.0", 351 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", 352 | "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", 353 | "dev": true, 354 | "engines": { 355 | "node": ">=10.0.0" 356 | }, 357 | "peerDependencies": { 358 | "bufferutil": "^4.0.1", 359 | "utf-8-validate": "^5.0.2" 360 | }, 361 | "peerDependenciesMeta": { 362 | "bufferutil": { 363 | "optional": true 364 | }, 365 | "utf-8-validate": { 366 | "optional": true 367 | } 368 | } 369 | }, 370 | "node_modules/zora": { 371 | "version": "5.0.3", 372 | "resolved": "https://registry.npmjs.org/zora/-/zora-5.0.3.tgz", 373 | "integrity": "sha512-3DAIXwF1LAM17da2WXbW3vxit/EuTjXFoLsDe14ZAWePaq7v7SETp4uBus2vwrl67KozYrLYWsGYJ4GOsM7NVA==", 374 | "dev": true 375 | } 376 | }, 377 | "dependencies": { 378 | "@lorenzofox3/for-await": { 379 | "version": "0.2.2", 380 | "resolved": "https://registry.npmjs.org/@lorenzofox3/for-await/-/for-await-0.2.2.tgz", 381 | "integrity": "sha512-IKIjQAyl6yu/kWTCqpc4QQwkA048pgsvvwxb8bMoqC4NBdypVBGj0wxFTL882sw8WzMSpov3g/pP6RxPhqbJHA==", 382 | "dev": true 383 | }, 384 | "esbuild": { 385 | "version": "0.14.43", 386 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.43.tgz", 387 | "integrity": "sha512-Uf94+kQmy/5jsFwKWiQB4hfo/RkM9Dh7b79p8yqd1tshULdr25G2szLz631NoH3s2ujnKEKVD16RmOxvCNKRFA==", 388 | "dev": true, 389 | "requires": { 390 | "esbuild-android-64": "0.14.43", 391 | "esbuild-android-arm64": "0.14.43", 392 | "esbuild-darwin-64": "0.14.43", 393 | "esbuild-darwin-arm64": "0.14.43", 394 | "esbuild-freebsd-64": "0.14.43", 395 | "esbuild-freebsd-arm64": "0.14.43", 396 | "esbuild-linux-32": "0.14.43", 397 | "esbuild-linux-64": "0.14.43", 398 | "esbuild-linux-arm": "0.14.43", 399 | "esbuild-linux-arm64": "0.14.43", 400 | "esbuild-linux-mips64le": "0.14.43", 401 | "esbuild-linux-ppc64le": "0.14.43", 402 | "esbuild-linux-riscv64": "0.14.43", 403 | "esbuild-linux-s390x": "0.14.43", 404 | "esbuild-netbsd-64": "0.14.43", 405 | "esbuild-openbsd-64": "0.14.43", 406 | "esbuild-sunos-64": "0.14.43", 407 | "esbuild-windows-32": "0.14.43", 408 | "esbuild-windows-64": "0.14.43", 409 | "esbuild-windows-arm64": "0.14.43" 410 | } 411 | }, 412 | "esbuild-darwin-64": { 413 | "version": "0.14.43", 414 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.43.tgz", 415 | "integrity": "sha512-/3PSilx011ttoieRGkSZ0XV8zjBf2C9enV4ScMMbCT4dpx0mFhMOpFnCHkOK0pWGB8LklykFyHrWk2z6DENVUg==", 416 | "dev": true, 417 | "optional": true 418 | }, 419 | "fsevents": { 420 | "version": "2.3.2", 421 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 422 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 423 | "dev": true, 424 | "optional": true 425 | }, 426 | "function-bind": { 427 | "version": "1.1.1", 428 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 429 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 430 | "dev": true 431 | }, 432 | "has": { 433 | "version": "1.0.3", 434 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 435 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 436 | "dev": true, 437 | "requires": { 438 | "function-bind": "^1.1.1" 439 | } 440 | }, 441 | "is-buffer": { 442 | "version": "1.1.6", 443 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 444 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 445 | }, 446 | "is-core-module": { 447 | "version": "2.9.0", 448 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", 449 | "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", 450 | "dev": true, 451 | "requires": { 452 | "has": "^1.0.3" 453 | } 454 | }, 455 | "is-even": { 456 | "version": "1.0.0", 457 | "resolved": "https://registry.npmjs.org/is-even/-/is-even-1.0.0.tgz", 458 | "integrity": "sha512-LEhnkAdJqic4Dbqn58A0y52IXoHWlsueqQkKfMfdEnIYG8A1sm/GHidKkS6yvXlMoRrkM34csHnXQtOqcb+Jzg==", 459 | "requires": { 460 | "is-odd": "^0.1.2" 461 | } 462 | }, 463 | "is-number": { 464 | "version": "3.0.0", 465 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", 466 | "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", 467 | "requires": { 468 | "kind-of": "^3.0.2" 469 | } 470 | }, 471 | "is-odd": { 472 | "version": "0.1.2", 473 | "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-0.1.2.tgz", 474 | "integrity": "sha512-Ri7C2K7o5IrUU9UEI8losXJCCD/UtsaIrkR5sxIcFg4xQ9cRJXlWA5DQvTE0yDc0krvSNLsRGXN11UPS6KyfBw==", 475 | "requires": { 476 | "is-number": "^3.0.0" 477 | } 478 | }, 479 | "kind-of": { 480 | "version": "3.2.2", 481 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 482 | "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", 483 | "requires": { 484 | "is-buffer": "^1.1.5" 485 | } 486 | }, 487 | "nanoid": { 488 | "version": "3.3.7", 489 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 490 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 491 | "dev": true 492 | }, 493 | "path-parse": { 494 | "version": "1.0.7", 495 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 496 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 497 | "dev": true 498 | }, 499 | "picocolors": { 500 | "version": "1.0.0", 501 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 502 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", 503 | "dev": true 504 | }, 505 | "playwright": { 506 | "version": "1.22.2", 507 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.22.2.tgz", 508 | "integrity": "sha512-hUTpg7LytIl3/O4t0AQJS1V6hWsaSY5uZ7w1oCC8r3a1AQN5d6otIdCkiB3cbzgQkcMaRxisinjMFMVqZkybdQ==", 509 | "dev": true, 510 | "requires": { 511 | "playwright-core": "1.22.2" 512 | } 513 | }, 514 | "playwright-core": { 515 | "version": "1.22.2", 516 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.22.2.tgz", 517 | "integrity": "sha512-w/hc/Ld0RM4pmsNeE6aL/fPNWw8BWit2tg+TfqJ3+p59c6s3B6C8mXvXrIPmfQEobkcFDc+4KirNzOQ+uBSP1Q==", 518 | "dev": true 519 | }, 520 | "postcss": { 521 | "version": "8.4.32", 522 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", 523 | "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", 524 | "dev": true, 525 | "requires": { 526 | "nanoid": "^3.3.7", 527 | "picocolors": "^1.0.0", 528 | "source-map-js": "^1.0.2" 529 | } 530 | }, 531 | "resolve": { 532 | "version": "1.22.0", 533 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", 534 | "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", 535 | "dev": true, 536 | "requires": { 537 | "is-core-module": "^2.8.1", 538 | "path-parse": "^1.0.7", 539 | "supports-preserve-symlinks-flag": "^1.0.0" 540 | } 541 | }, 542 | "rollup": { 543 | "version": "2.75.6", 544 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.6.tgz", 545 | "integrity": "sha512-OEf0TgpC9vU6WGROJIk1JA3LR5vk/yvqlzxqdrE2CzzXnqKXNzbAwlWUXis8RS3ZPe7LAq+YUxsRa0l3r27MLA==", 546 | "dev": true, 547 | "requires": { 548 | "fsevents": "~2.3.2" 549 | } 550 | }, 551 | "source-map-js": { 552 | "version": "1.0.2", 553 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 554 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 555 | "dev": true 556 | }, 557 | "supports-preserve-symlinks-flag": { 558 | "version": "1.0.0", 559 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 560 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 561 | "dev": true 562 | }, 563 | "vite": { 564 | "version": "2.9.18", 565 | "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.18.tgz", 566 | "integrity": "sha512-sAOqI5wNM9QvSEE70W3UGMdT8cyEn0+PmJMTFvTB8wB0YbYUWw3gUbY62AOyrXosGieF2htmeLATvNxpv/zNyQ==", 567 | "dev": true, 568 | "requires": { 569 | "esbuild": "^0.14.27", 570 | "fsevents": "~2.3.2", 571 | "postcss": "^8.4.13", 572 | "resolve": "^1.22.0", 573 | "rollup": ">=2.59.0 <2.78.0" 574 | } 575 | }, 576 | "ws": { 577 | "version": "8.8.0", 578 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", 579 | "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", 580 | "dev": true, 581 | "requires": {} 582 | }, 583 | "zora": { 584 | "version": "5.0.3", 585 | "resolved": "https://registry.npmjs.org/zora/-/zora-5.0.3.tgz", 586 | "integrity": "sha512-3DAIXwF1LAM17da2WXbW3vxit/EuTjXFoLsDe14ZAWePaq7v7SETp4uBus2vwrl67KozYrLYWsGYJ4GOsM7NVA==", 587 | "dev": true 588 | } 589 | } 590 | } 591 | --------------------------------------------------------------------------------