Compare commits

..

No commits in common. "master" and "0327de8bdc5a975e1c9f5e486de1b638785d32cc" have entirely different histories.

35 changed files with 733 additions and 5733 deletions

View file

@ -1,44 +0,0 @@
on:
push:
branches:
- master
jobs:
deploy:
runs-on: arch
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Install dependencies
run: time pnpm install
- name: Run Tests
run: pnpm run test --ignore-only
- name: Deply if new version
run: |
echo ""
echo "------------------------------------"
echo ""
CURR_VER="$(cat package.json | jq -r .name)_v$(cat package.json | jq -r .version)"
CURR_NAME="$(cat package.json | jq -r .name) v$(cat package.json | jq -r .version)"
echo "Checking https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases for name ${CURR_NAME}"
if curl -s -X GET -H "Authorization: token ${{ secrets.deploytoken }}" https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases | grep -o "\"name\":\"${CURR_NAME}\"" > /dev/null; then
echo "Skipping ${{ github.job }} since $CURR_NAME already exists";
exit;
fi
echo "New release ${CURR_VER} found, beginning deployment"
echo "Creating ${CURR_VER} release on forgejo"
curl \
-X POST \
-H "Authorization: token ${{ secrets.deploytoken }}" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/${{ github.repository }}/releases \
-d "{\"tag_name\":\"${CURR_VER}\",\"name\":\"${CURR_NAME}\",\"body\":\"Automatic release from Appveyor from ${{ github.sha }} :\n\n${{ github.event.head_commit.message }}\"}" | jq
echo "//registry.npmjs.org/:_authToken=${{ secrets.npmtoken }}" > ~/.npmrc
echo "Publishing new version to npm"
npm publish

2
.gitignore vendored
View file

@ -102,5 +102,3 @@ dist
# TernJS port file
.tern-port
test/watch/__TREE__

1
.npmrc
View file

@ -1 +0,0 @@
package-lock=false

445
README.md
View file

@ -1,4 +1,4 @@
# eltro [![Build status](https://ci.nfp.is/api/projects/status/n7ufp6gi48rc3bs9?svg=true)](https://ci.nfp.is/project/AppVeyor/eltro)
# eltro
Eltro is a no-nonsense, no dependancy, small test framework created to use in node 13 or higher using ECM modules.
# Installation
@ -24,7 +24,7 @@ $ mkdir test
Next in your favourite editor, create `test/test.mjs`:
```javascript
```node
import { Eltro as t, assert} from 'eltro'
t.describe('Array', function() {
@ -65,67 +65,23 @@ $ npm test
1 passing (3ms)
```
# Watch
You can also run eltro in watch mode. Update your package.json and add the following:
```json
{
/* ... */
"scripts": {
"test": "eltro",
"test:watch": "eltro --watch my_watch_name",
},
"watch": {
"my_watch_name": {
"patterns": [ "src", "test" ],
"extensions": "js,mjs"
}
},
/* ... */
}
```
Then add `--watch my_watch_name` to your eltro command (as seen in the above example) and you're good to go:
```bash
$ npm test:watch
test/test.mjs
√ Array #indexOf() should return -1 when value is not present
1 passing (3ms)
[my_watch_name] 09:49:38: Ran successfully. Waiting for file changes before running again...
```
You can also run your own npm command while using the eltro file watcher like so:
```bash
$ eltro --watch my_watch_name --npm build
```
# Assertions
# Assertions
Not only does eltro allow you to use any assertion library of your own choosing, it also comes with it's own assertion library based on node's default [assert](https://nodejs.org/api/assert.html) with a few extra methods:
* `assert.equalWithMargin(value, test, margin, [message])`: Check if number value is equal to test with error margin.
* `assert.notOk(value, [message])`: Assert value is not ok.
* `assert.match(value, test, [message])`: Check if value matches RegExp test.
* `assert.notMatch(value, [message])`: Check if value does not match RegExp test.
* `assert.throwsAndCatch(fn, [message])`: Checks if function fn throws and returns the thrown error.
* `assert.isFulfilled(promise, [message])`: Assert the promise resolves.
* `assert.isRejected(promise, [message])`: Assert the promise gets rejects.
# Asynchronous Code
# Asynchronous Code
Eltro supports any type of asynchronous code testing. It can either be done by adding a parameter to the function (usually done) that gets called once the tests done but eltro also supports promises.
Example of testing using done:
```javascript
```node
import { Eltro as t, assert} from 'eltro'
t.describe('User', function() {
@ -143,7 +99,7 @@ t.describe('User', function() {
Alternatively, just use the done() callback directly (which will handle an error argument, if it exists):
```javascript
```node
import { Eltro as t, assert} from 'eltro'
t.describe('User', function() {
@ -158,7 +114,7 @@ t.describe('User', function() {
Or another alternative is to use promises and return a promise directly:
```javascript
```node
import { Eltro as t, assert} from 'eltro'
t.test('should complete this test', function(done) {
@ -170,37 +126,13 @@ t.test('should complete this test', function(done) {
Which works well with `async/await` like so:
```javascript
```node
t.test('async test', async function() {
let user = await User.find({ username: 'test' })
assert.ok(user)
})
```
# Spying and stubbing
Inspired by sinon js, this library comes with pre-built simple sinon-like style spy() and stub()
```javascript
import { assert, spy, stub } from 'eltro'
let myFunc = spy()
let myStub = stub()
myFunc(1)
myFunc(2)
myFunc(3)
myStub.returns('world')
let out = myStub('hello')
assert.strictEqual(out, 'world')
assert.strictEqual(myFunc.getCall(0), 1)
assert.strictEqual(myFunc.getCall(1), 2)
assert.strictEqual(myFunc.getCall(2), 3)
assert.strictEqual(myFunc.callCount, 3)
assert.strictEqual(myStub.callCount, 1)
```
# Api
### t.test(message, func)
@ -211,7 +143,7 @@ Queue up the `func` as a test with the specified messagt.
In case you wanna describe a bunch of tests, you can add them inside `func` and it will have the specified `message` prepended before every test:
```javascript
```node
import { Eltro as t, assert} from 'eltro'
function someFunction() { return true }
@ -235,7 +167,7 @@ will output:
Queue up the `func` to run before any test or groups within current active group.
```javascript
```node
import { Eltro as t, assert} from 'eltro'
t.before(function() {
@ -267,7 +199,7 @@ t.describe('#anotherTest()', function() {
Queue up the `func` to run after any test or groups within current active group.
```javascript
```node
import { Eltro as t, assert} from 'eltro'
t.after(function() {
@ -295,77 +227,13 @@ t.describe('#anotherTest()', function() {
})
```
### t.beforeEach(func)
Queue up the `func` to run before each test or groups within current active group.
```javascript
import { Eltro as t, assert} from 'eltro'
t.beforeEach(function() {
// Prepare something before each of the following tests
})
t.describe('#myTest()', function() {
t.beforeEach(function() {
// Runs before every test in this group
})
t.test('true should always be true', function() {
assert.strictEqual(true, true)
})
})
t.describe('#anotherTest()', function() {
t.beforeEach(function() {
// Runs before every test in this group
})
t.test('false should always be false', function() {
assert.strictEqual(false, false)
})
})
```
### t.afterEach(func)
Queue up the `func` to run after every test or groups within current active group.
```javascript
import { Eltro as t, assert} from 'eltro'
t.afterEach(function() {
// After we finish each individual test below, this gets run
})
t.describe('#myTest()', function() {
t.afterEach(function() {
// Runs after each text in this group
})
t.test('true should always be true', function() {
assert.strictEqual(true, true)
})
})
t.describe('#anotherTest()', function() {
t.afterEach(function() {
// Runs after each text in this group
})
t.test('false should always be false', function() {
assert.strictEqual(false, false)
})
})
```
### t.only()
Eltro supports exclusivity when running tests. When specified, only tests marked with only will be run.
You can do exclusivity on tests by adding `.only()` in front of describe, after or before the test like so:
```javascript
```node
t.only().describe('Only these will run', function() {
t.test('this one', function() { assert.strictEqual(true, true) })
t.test('and this one', function() { assert.strictEqual(true, true) })
@ -374,7 +242,7 @@ t.only().describe('Only these will run', function() {
You can also put it on individual test like so
```javascript
```node
t.test('Only run this test', function() {
assert.strictEqual(true, true)
}).only()
@ -382,7 +250,7 @@ t.test('Only run this test', function() {
or like so:
```javascript
```node
t.only().test('Only run this test', function() {
assert.strictEqual(true, true)
})
@ -392,7 +260,7 @@ t.only().test('Only run this test', function() {
You can skip tests easily by adding `.skip()` before describe, before or after the test like so:
```javascript
```node
t.skip().describe('None of these will run', function() {
t.test('not this', function() { assert.strictEqual(true, true) })
t.test('or this one', function() { assert.strictEqual(true, true) })
@ -401,7 +269,7 @@ t.skip().describe('None of these will run', function() {
You can also do it on individual tests like so:
```javascript
```node
t.test('Skip due to something being broken', function() {
BrokenFunction()
}).skip()
@ -409,7 +277,7 @@ t.test('Skip due to something being broken', function() {
or like so:
```javascript
```node
t.skip().test('Skip this', function() { ... })
```
@ -417,7 +285,7 @@ t.skip().test('Skip this', function() { ... })
Tests can take a long time. By default, eltro will cancel a test if it takes longer than 2 seconds. You can however override this by calling the timeout function after or before the test or before the describe with the specified duration in milliseconds like so:
```javascript
```node
t.timeout(5000).describe('These will all have same timeout', function() {
t.test('One slow function', async function() { ... })
t.test('Another slow function', async function() { ... })
@ -426,7 +294,7 @@ t.timeout(5000).describe('These will all have same timeout', function() {
Or apply to individual test like so:
```javascript
```node
t.test('This is a really long test', async function() {
await DoSomethingForReallyLongTime()
}).timeout(5000) // 5 seconds
@ -434,277 +302,6 @@ t.test('This is a really long test', async function() {
or like so:
```javascript
```node
t.timeout(5000).test('A long test', async function() { ... })
```
# Assert
Eltro comes with an extended version of node's built-in assertion library.
You can start using them by simply importing it with eltro test runner:
```javascript
import { assert } from 'eltro'
```
### assert.notOk(value[, message])
Tests if value is a falsy value using `Boolean(value) == false`
```javascript
assert.notOk(false) // ok
assert.notOk(null) // ok
assert.notOk(undefined) // ok
assert.notOk([]) // throws
```
### assert.match(value, test[, message])
Test if the string value has a regex match of test
```javascript
assert.match('asdf', /a/) // ok
assert.match('hello world', /hello/) // ok
assert.match('something', /else/) // throws
```
### assert.notMatch(value, test[, message])
Test if the string value does not regex match the test
```javascript
assert.notMatch('asdf', /b/) // ok
assert.notMatch('something', /else/) // ok
assert.notMatch('hello world', /hello/) // throws
```
### assert.isFulfilled(promise[, message])
Tests to make sure the promise gets fulfilled successfully and
returns the final result.
```javascript
await assert.isFulfilled(Promise.resolve(null)) // ok
await assert.isFulfilled(() => { throw new Error() }) // throws
```
### assert.isRejected(promise[, message])
Tests to make sure the promise gets rejected and returns the error
or value that was rejected
```javascript
let val = await assert.isRejected(Promise.reject('asdf')) // ok
assert.strictEqual(val, 'asdf')
let err = await assert.isRejected(() => { throw new Error('hello') }) // ok
assert.strictEqual(err.message, 'hello')
```
### assert.throwsAndCatch(fn[, message])
Tests to make sure the function throws an exception. The important feature is this returns the original error that was thrown.
```javascript
let err = assert.throwsAndCatch(() => { throw new Error('hello world') }) // ok
assert.strictEqual(err.message, 'hello world')
```
# Sinon-like spy() stub()
Using sinon-inspired mechanics for spying on calls as well as being able
to stub existing functionality, eltro comes with a handy little copy-cat.
Functionality-wise, the difference between spy() and stub() are none.
Both will do the exact same thing, the naming differention is simply to allow
the resulting code to speak about its purpose.
To create a stub or a spy, simply import it and call it like so:
```javascript
import { spy, stub } from 'eltro'
let spying = spy()
let fn = stub()
```
Each call to stub or spy is an array list of the passed-on arguments:
```javascript
let spying = spy()
spying('hello', 'world')
assert.strictEqual(spying.lastCall[0], 'hello')
assert.strictEqual(spying.lastCall[1], 'world')
```
### lastCall
Returns the last call that was made to the spy or stub:
```javascript
let spying = spy()
spying('a')
spying('b')
spying('c')
assert.strictEqual(spying.lastCall[0], 'c')
```
### called
Boolean variable that gets flipped once it gets called at least once
```javascript
let spying = spy()
assert.strictEqual(spying.called, false)
spying('a')
assert.strictEqual(spying.called, true)
spying('b')
spying('c')
assert.strictEqual(spying.called, true)
```
### callCount
The number of times it's been called
```javascript
let spying = spy()
assert.strictEqual(spying.callCount, 0)
spying('a')
assert.strictEqual(spying.callCount, 1)
spying('b')
spying('c')
assert.strictEqual(spying.callCount, 3)
```
### returns(data)
Specifies what value the stub or spy should return when it gets called.
```javascript
let fn = stub()
fn.returns('a')
assert.strictEqual(fn(), 'a')
assert.strictEqual(fn(), 'a')
```
### throws(data)
Specifies what value the stub or spy should throw when it gets called.
```javascript
let fn = stub()
fn.throws(new Error('b'))
try {
fn()
} catch (err) {
assert.strictEqual(err.message, 'b')
}
```
### resolves(data)
Specifies what value the stub or spy should return wrapped in a promise.
```javascript
let fn = stub()
fn.resolves('a')
fn().then(function(data) {
assert.strictEqual(data, 'a')
})
```
### rejects(data)
Specifies what value the stub or spy should reject, wrapped in a promise.
```javascript
let fn = stub()
fn.rejects('nope')
fn().catch(function(data) {
assert.strictEqual(data, 'nope')
})
```
### returnWith(fn)
Specify custom function to be called whenever the stub or spy gets called.
```javascript
let fn = stub()
fn.returnWith(function(a) {
if (a === 'a') return true
return false
})
assert.strictEqual(fn(), false)
assert.strictEqual(fn('b'), false)
assert.strictEqual(fn('a'), true)
assert.strictEqual(fn.callCount, 3)
```
### getCall(index) getCallN(num)
Get a specific call. The `getCall` gets a zero-based index call while the `getCallN(num)` gets the more natural number call
```javascript
let spying = spy()
spying('a')
spying('b')
spying('c')
assert.strictEqual(spying.getCall(0), 'a')
assert.strictEqual(spying.getCall(1), 'b')
assert.strictEqual(spying.getCallN(1), 'a')
assert.strictEqual(spying.getCallN(2), 'b')
```
### onCall(index) onCallN(num)
Overwrite behavior for a specific numbered call. Just like with getCall/getCallN, the onCall is zero-indexed number of the call you want to specify custom behavior while onCallN is the more natural number of the call you want to specify custom behavior.
Note, when called with null, it specifies the default behavior.
```javascript
let fnOne = stub()
let fnTwo = stub()
fnOne.onCall(1).returns('b')
.onCall().returns('a')
fnTwo.onCallN(2).returns('two')
.onCallN().returns('one')
assert.strictEqual(fnOne(), 'a')
assert.strictEqual(fnOne(), 'b')
assert.strictEqual(fnOne(), 'a')
assert.strictEqual(fnTwo(), 'one')
assert.strictEqual(fnTwo(), 'two')
assert.strictEqual(fnTwo(), 'one')
```
### findCall(fn)
Search for the first call when `fn(call)` returns `true`. Essentially a filter to search for a specific call that matches whatever call you're searching for.
```javascript
let evnt = stub()
evnt('onclick', 'one')
evnt('onerror', 'two')
evnt('something', 'three')
evnt('onpress', 'four')
evnt('else', 'five')
let foundPressCall = evnt.findCall(function(call) { return call[0] === 'onpress' })
assert.strictEqual(foundPressCall[0], 'onpress')
assert.strictEqual(foundPressCall[1], 'four')
```

79
cli.mjs
View file

@ -6,6 +6,15 @@ const [,, ...args] = process.argv
import e from './lib/eltro.mjs'
import { CLI, printError } from './lib/cli.mjs'
e.begin()
const cli = new CLI(e)
cli.parseOptions(args)
if (cli.errored) {
PrintHelp()
}
function PrintHelp() {
console.log('')
console.log('Usage: eltro <options> <files>')
@ -14,11 +23,6 @@ function PrintHelp() {
console.log('where <options> can be any of the following:')
console.log(' -r, --reporter - Specify the reporter to use.')
console.log(' Supported reporters: list, dot')
console.log(' -t, --timeout - Specify the timeout for tests in ms.')
console.log(' Default value is 2000ms')
console.log(' -w, --watch - specify which group of files to watch from package.json')
console.log(' ')
console.log(' --ignore-only - Specify to ignore any .only() tests found')
console.log('')
console.log('eltro test/mytest.mjs')
console.log('eltro dot test/*.mjs')
@ -27,52 +31,41 @@ function PrintHelp() {
process.exit(1)
}
function showErrorAndExit(message = '', err = null, code = 1, clean = false) {
if (!clean) {
cli.processTargets().then(function() {
if (!cli.files.length) {
console.log('')
}
if (message) {
console.error(`\x1b[31m${message}\x1b[0m`)
}
if (err) {
printError(err, '', clean)
if (err.inner) {
return showErrorAndExit(null, err.inner, code, true)
}
} else {
console.log('No files were found with pattern', cli.targets.join(','))
PrintHelp()
}
process.exit(code)
}
const cli = new CLI(e)
cli.parseOptions(args)
.catch(function(err) { showErrorAndExit(err.message) })
.then(function() {
return cli.startWatcher()
})
.catch(function(err) { showErrorAndExit('Unknown error while starting watcher', err) })
.then(function() {
return cli.getFiles()
})
.catch(function(err) { showErrorAndExit('Unknown error while processing arguments', err) })
.then(function() {
if (!cli.files.length && cli.run === 'test') {
showErrorAndExit('No files were found with pattern', cli.targets.join(','))
}
return cli.loadFiles()
.then(function() {
e.reporter = cli.reporter
return e.run()
.catch(function(err) {
console.log('')
console.error('\x1b[31mUnknown error occured while running the tests\x1b[0m')
printError(err)
process.exit(1)
})
}, function(err) {
console.log('')
console.error('\x1b[31m' + err.message + '\x1b[0m')
printError(err.inner)
process.exit(1)
})
}, function(err) {
console.log('')
console.error('\x1b[31mUnknown error while processing arguments\x1b[0m')
printError(err)
process.exit(1)
})
.catch(function(err) { showErrorAndExit('', err) })
.then(function() {
return cli.beginRun()
})
.catch(function(err) { showErrorAndExit('Unknown error occured while running the tests', err) })
.then(function(stats) {
if (stats.failed > 0) {
process.exit(10)
}
process.exit(0)
}, function(err) {
console.error('\x1b[31mInternal error occured:\x1b[0m', err)
process.exit(2)
})

63
eltro.d.ts vendored
View file

@ -1,63 +0,0 @@
import NodeAssert from 'assert';
type ExtendedAssert = {
notOk(value: unknown, message?: string | Error): void,
match(value: string, test: RegExp, message?: string | Error): void,
notMatch(value: string, test: RegExp, message?: string | Error): void,
throwsAndCatch(fn: () => unknown, message?: string | Error): Error,
equalWithMargin(value: number, test: number, margin?: number, message?: string | Error): void,
isFulfilled<T>(promise: Promise<T>, message?: string | Error): Promise<T>,
isRejected(promise: Promise<unknown>, message?: string | Error): Promise<unknown>,
}
export const assert: typeof NodeAssert & ExtendedAssert
type Spy = {
(): unknown
lastCall: null | Array<unknown>
called: boolean
callCount: number
calls: Array<Array<unknown>>
findCall(fn: (args: Array<unknown>) => boolean): null | Array<unknown>
getCall(index: number): null | Array<unknown>
getCallN(index: number): null | Array<unknown>
onCall(index: number): Spy
onCallN(index: number): Spy
reset(): Spy
returns(data: unknown): Spy
throws(data: unknown): Spy
resolves(data: unknown): Spy
rejects(data: unknown): Spy
returnWith(fn: (...args: unknown[]) => unknown): Spy
}
export function spy(): Spy
export function stub(fun?: (...args: unknown[]) => unknown): Spy
type Test = {
timeout(timeout: number): void
skip(): void
only(): void
}
type TestResult = void | Promise<unknown>
type Callback = {
(): void
wrap(fn: () => unknown): void
finish(fn: () => unknown): void
}
type Eltro = {
before(fn: () => TestResult): Test
after(fn: () => TestResult): Test
beforeEach(fn: () => TestResult): Test
afterEachEach(fn: () => TestResult): Test
describe(name: string, fn: () => void)
timeout(timeout: number): Eltro
skip(): Eltro
only(): Eltro
test(name: string, func: (cb?: Callback) => TestResult): Test
}
export const Eltro: Eltro

View file

@ -48,23 +48,6 @@ assert.notMatch = (value, test, message) => {
fail(m);
}
assert.throwsAndCatch = (fn, message) => {
let err = null
assert.throws(fn, function(error) {
err = error
return true
}, message)
return err
}
assert.equalWithMargin = (value, test, margin = 0.005, message) => {
assert.strictEqual(typeof value, 'number', 'Value should be number')
assert.strictEqual(typeof test, 'number', 'Test should be number')
let difference = Math.abs(value - test)
assert.ok(difference <= margin, message || `${value} ± ${margin} != ${test} (difference of ${difference} > ${margin})`)
}
assert.isFulfilled = (promise, message) => {
return Promise.resolve(true)
.then(() => promise)

View file

@ -1,37 +0,0 @@
export function runWithCallbackSafe(test) {
let finished = false
return new Promise(function(res, rej) {
try {
let cb = function(err) {
if (err) {
return rej(err)
}
res()
}
let safeWrap = function(fn, ...args) {
try {
return fn(...args)
}
catch (err) {
return rej(err)
}
}
let safeWrapInFunction = function(finish, fn) {
return function(...args) {
safeWrap(fn, ...args)
if (finish && !finished) { res() }
}
}
cb.wrap = safeWrapInFunction.bind(this, false)
cb.finish = safeWrapInFunction.bind(this, true)
cb.safeWrap = safeWrap
test.func(cb)
} catch (err) {
rej(err)
}
})
.then(
function() { finished = true },
function(err) { finished = true; return Promise.reject(err) }
)
}

View file

@ -1,189 +1,47 @@
import path from 'path'
import fs from 'fs'
import fsPromise from 'fs/promises'
import cluster from 'cluster'
import child_process from 'child_process'
import kill from './kill.mjs'
import Watcher, { EVENT_REMOVE, EVENT_UPDATE } from './watch/index.mjs'
export const MESSAGE_FILES_REQUEST = 'message_files_request'
export const MESSAGE_FILES_PAYLOAD = 'message_files_payload'
export const MESSAGE_RUN_FINISHED = 'message_run_finished'
export function createMessage(type, data = null) {
return {
messageType: type,
data: data,
}
}
const RegexCache = new Map()
function targetToRegex(target) {
let regex = RegexCache.get(target)
if (!regex) {
let parsed = target.startsWith('./') ? target.slice(2) : target
parsed = parsed.endsWith('/') ? parsed + '*' : parsed
RegexCache.set(target, regex = new RegExp('^' +
parsed.replace(/\./g, '\\.')
.replace(/\*\*\/?/g, '&&&&&&&&&&&&&')
.replace(/\*/g, '[^/]+')
.replace(/&&&&&&&&&&&&&/g, '.*')
+ '$'
))
}
return regex
}
export function CLI(e, overrides = {}) {
export function CLI(e) {
this.e = e
this.ac = new AbortController()
this.cluster = overrides.cluster || cluster
this.logger = overrides.logger || console
this.child_process = overrides.child_process || child_process
this.process = overrides.process || process
this.kill = overrides.kill || kill
this.importer = overrides.importer
this.loadDefaults()
}
CLI.prototype.loadDefaults = function() {
// Eltro specific options
this.reporter = 'list'
this.ignoreOnly = false
this.timeout = 2000
// Cli specific options
this.watch = null
this.watcher = null
this.worker = null
this.run = 'test'
this.isSlave = this.cluster.isWorker || false
this.targets = ['test/**']
this.files = []
}
CLI.prototype.fileMatchesTarget = function(path) {
for (let target of this.targets) {
if (targetToRegex(target).test(path)) {
return true
}
}
return false
this.errored = false
}
CLI.prototype.parseOptions = function(args) {
if (!args || !args.length) {
this.targets.push('test/**')
return Promise.resolve()
this.errored = false
return
}
this.errored = false
this.targets.splice(0, this.targets.length)
for (let i = 0; i < args.length; i++) {
if (args[i] === '-r' || args[i] === '--reporter') {
if (!args[i + 1] || (args[i + 1] !== 'list' && args[i + 1] !== 'dot')) {
return Promise.reject(new Error('Reporter was missing or invalid. Only "list" and "dot" are supported.'))
this.errored = true
return
}
this.reporter = args[i + 1]
i++
} else if (args[i] === '-t' || args[i] === '--timeout') {
if (!args[i + 1] || isNaN(Number(args[i + 1]))) {
return Promise.reject(new Error('Timeout was missing or invalid'))
}
this.timeout = Number(args[i + 1])
i++
} else if (args[i] === '-w' || args[i] === '--watch') {
if (!args[i + 1] || args[i + 1][0] === '-') {
return Promise.reject(new Error('Watch was missing or invalid'))
}
this.watch = args[i + 1]
i++
} else if (args[i] === '-n' || args[i] === '--npm') {
if (!args[i + 1] || args[i + 1][0] === '-') {
return Promise.reject(new Error('Npm was missing or invalid'))
}
this.run = args[i + 1]
i++
} else if (args[i] === '--ignore-only') {
this.ignoreOnly = true
} else if (args[i][0] === '-') {
return Promise.reject(new Error(`Unknown option ${args[i]}`))
this.errored = true
return
} else {
this.targets.push(args[i])
}
}
if (!this.targets.length && this.run === 'test') {
if (!this.targets.length) {
this.targets.push('test/**')
}
return Promise.resolve()
}
CLI.prototype.startWatcher = async function() {
if (!this.watch || this.isSlave) {
return Promise.resolve()
}
let packageJson
try {
packageJson = JSON.parse(await fsPromise.readFile('package.json'))
} catch (err) {
throw new Error(`package.json was missing or invalid JSON: ${err.message}`)
}
let currentGroup = packageJson.watch && packageJson.watch[this.watch]
if (!currentGroup || !currentGroup.patterns) {
throw new Error(`package.json was missing watch property or missing ${this.watch} in watch or missing pattern`)
}
if (!currentGroup.extensions) {
currentGroup.extensions = 'js,mjs'
} else if (!currentGroup.extensions.match(/^([a-zA-Z]{2,3})(,[a-zA-Z]{2,3})*$/)) {
throw new Error(`package.json watch ${this.watch} extension "${currentGroup.extensions}" was invalid`)
}
return new Promise((res, rej) => {
this.watcher = new Watcher(currentGroup.patterns, {
quickNativeCheck: true,
delay: currentGroup.delay || 200,
skip: function(name) {
return name.indexOf('node_modules') >= 0
},
filter: new RegExp(currentGroup.extensions.split(',').map(x => `(\\.${x}$)`).join('|'))
})
this.watcher.once('error', rej)
this.watcher.once('ready', () => {
this.watcher.off('error', rej)
res()
})
})
}
CLI.prototype.getFiles = function() {
if (this.isSlave) {
return this._askMasterForFiles()
} else {
return this._processTargets()
}
}
CLI.prototype._askMasterForFiles = function() {
return new Promise(res => {
const handler = (payload) => {
if (isMessageInvalid(payload, MESSAGE_FILES_PAYLOAD)) return
this.process.off('message', handler)
res(this.files = payload.data)
}
this.process.on('message', handler)
this.process.send(createMessage(MESSAGE_FILES_REQUEST))
})
}
CLI.prototype._processTargets = function() {
CLI.prototype.processTargets = function() {
this.files.splice(0, this.files.length)
if (!this.targets.length) {
@ -191,28 +49,22 @@ CLI.prototype._processTargets = function() {
}
return Promise.all(this.targets.map((target) => {
return getFilesFromTarget(this.files, target)
return getFiles(this.files, target)
})).then(() => {
if (!this.files.length) {
this.errored = 'empty'
}
return this.files
})
}
CLI.prototype.loadFiles = async function() {
if (!this.isSlave && this.watch) {
return Promise.resolve()
}
this.e.begin()
let cwd = this.process.cwd()
let cwd = process.cwd()
for (let i = 0; i < this.files.length; i++) {
if (this.files[i].endsWith('.mjs') || this.files[i].endsWith('.js')) {
try {
this.e.setFilename(this.files[i])
await this.import('file:///' + path.join(cwd, this.files[i]))
await import('file:///' + path.join(cwd, this.files[i]))
this.e.resetFilename()
} catch (e) {
let newError = new Error(`Error while loading ${this.files[i]}`)
@ -223,131 +75,6 @@ CLI.prototype.loadFiles = async function() {
}
}
CLI.prototype.beginRun = async function() {
if (this.watcher) {
return this._runWorker()
} else {
return this._runTests()
}
}
CLI.prototype._runTests = function() {
this.e.reporter = this.reporter
this.e.ignoreOnly = this.ignoreOnly
this.e.__timeout = this.timeout
return this.e.run().then((stats) => {
if (this.isSlave) {
this.process.send(createMessage(MESSAGE_RUN_FINISHED, { stats: stats }))
}
return stats
})
}
CLI.prototype._runWorker = function() {
let lastStats = null
const messageHandler = (payload) => {
if (isMessageInvalid(payload, MESSAGE_RUN_FINISHED)) return
lastStats = payload.data.stats
}
const changeHandler = (evt, name) => {
if (evt === EVENT_UPDATE && !this.files.includes(name) && this.fileMatchesTarget(name)) {
this.files.push(name)
} else if (evt === EVENT_REMOVE) {
let index = this.files.indexOf(name)
if (index >= 0) {
this.files.splice(index, 1)
}
}
}
const changedHandler = () => {
this.runProgram()
}
return new Promise(res => {
const cleanup = () => {
this.process.off('message', messageHandler)
this.watcher.off('change', changeHandler)
this.watcher.off('changed', changedHandler)
res(lastStats)
}
this.process.on('message', messageHandler)
this.watcher.on('change', changeHandler)
this.watcher.on('changed', changedHandler)
this.ac.signal.addEventListener('abort', cleanup, { once: true });
changedHandler()
})
}
CLI.prototype.runProgram = function() {
let runningTest = this.run === 'test'
if (this.worker) {
if (runningTest) {
return
} else {
this.kill(this.worker.pid)
}
}
let worker
if (runningTest) {
worker = this.worker = this.cluster.fork()
} else {
worker = this.worker = this.child_process.spawn('npm', ['run', this.run])
}
worker.once('exit', (exitCode) => {
if (this.worker !== worker) return
let currentTime = new Date().toISOString().split('T')[1].split('.')[0]
if (!runningTest) {
console.log()
}
if (exitCode > 0) {
console.error(`\x1b[31m[${this.watch}] ${currentTime}: Exited with error code ${exitCode}. Waiting for file changes before running again...\x1b[0m`)
} else {
console.error(`\x1b[32m[${this.watch}] ${currentTime}: Ran successfully. Waiting for file changes before running again...\x1b[0m`)
}
this.worker = null
})
if (runningTest) {
worker.on('message', (msg) => {
if (isMessageValid(msg, MESSAGE_FILES_REQUEST)) {
worker.send(createMessage(MESSAGE_FILES_PAYLOAD, this.files))
}
})
} else {
worker.stdout.on('data', (d) => { this.process.stdout.write(d.toString()) })
worker.stderr.on('data', (d) => { this.process.stderr.write(d.toString()) })
}
}
CLI.prototype.import = function(path) {
if (this.importer) {
return this.importer(path)
}
return import(path)
}
function isMessageInvalid(payload, messageType) {
if (!payload
|| typeof(payload) !== 'object'
|| typeof(payload.messageType) !== 'string'
|| payload.messageType !== messageType
|| (payload.data != null && typeof(payload.data) !== 'object')) {
return true
}
return false
}
function isMessageValid(payload, messageType) {
return !isMessageInvalid(payload, messageType)
}
function traverseFolder(files, curr, match, insidePath, grabAll, insideStar, includeFiles) {
return new Promise(function(resolve, reject) {
return fs.readdir(curr, function(err, data) {
@ -367,7 +94,7 @@ function traverseFolder(files, curr, match, insidePath, grabAll, insideStar, inc
if (stat.isDirectory() && grabAll) {
return res(traverseFolder(files, path.join(curr, file), match, path.join(insidePath, file), grabAll, insideStar, includeFiles))
} else if (stat.isDirectory() && match) {
return res(getFilesFromTarget(files, match, path.join(insidePath, file), grabAll, insideStar))
return res(getFiles(files, match, path.join(insidePath, file), grabAll, insideStar))
}
res(null)
})
@ -381,7 +108,7 @@ export function fileMatches(filename, match) {
return Boolean(filename.match(new RegExp(match.replace(/\./, '\\.').replace(/\*/, '.*'))))
}
export function getFilesFromTarget(files, match, insidePath, grabAll, insideStar) {
export function getFiles(files, match, insidePath, grabAll, insideStar) {
let isGrabbingAll = grabAll || false
let isStarred = insideStar || false
let cwd = process.cwd()
@ -410,7 +137,7 @@ export function getFilesFromTarget(files, match, insidePath, grabAll, insideStar
return traverseFolder(files, curr, splitted.slice(start + 1).join('/'), currPath, isGrabbingAll, isStarred, false)
.then(res, rej)
}
return getFilesFromTarget(files, splitted.slice(start + 1).join('/'), path.join(currPath, first), grabAll, isStarred)
return getFiles(files, splitted.slice(start + 1).join('/'), path.join(currPath, first), grabAll, isStarred)
.then(res, rej)
} else if (first.indexOf('*') >= 0) {
if (first === '**') {
@ -440,9 +167,9 @@ export function getFilesFromTarget(files, match, insidePath, grabAll, insideStar
})
}
export function printError(err, msg, clean = false) {
export function printError(err, msg) {
let before = msg || ''
if (!clean) console.error('')
console.error('')
console.error('\x1b[31m '
+ before + err.toString()
+ '\x1b[0m\n \x1b[90m'

47
lib/eltro.d.ts vendored
View file

@ -1,47 +0,0 @@
declare interface Stats {
passed: number
failed: number
skipped: number
}
declare class Eltro {
__timeout: number
hasExclusive: boolean
reporter: 'dot' | 'list'
Eltro: Eltro
fileGroupMap: Map<string, Group>
groups: Array<Group>
activeGroup: Group | null
failedTests: Array<Test>
hasTests: boolean
starting: boolean
ignoreOnly: boolean
logger: null | { log: function(...param): void }
filename: string
prefix: string
temporary: { timeout: number, skip: boolean, only: boolean }
describeTemporary: { timeout: number, skip: boolean, only: boolean }
__runTest(stats: Stats, test: Test, prefix: string = 'Test', child?: Test | null = null)
__runGroup(group: Group, stats: Stats)
begin()
run()
setFilename(filename: string)
resetFilename(filename: string)
before(fn: (done?: Function) => void | Promise)
after(fn: (done?: Function) => void | Promise)
beforeEach(fn: (done?: Function) => void | Promise)
afterEach(fn: (done?: Function) => void | Promise)
describe(name: string, fn: Function)
}
declare class Test {
}
declare class Group {
}
export default new Eltro()

View file

@ -1,6 +1,3 @@
/// <reference path="./eltro.d.ts"/>
import * as readline from 'readline'
import { runWithCallbackSafe } from './callback.mjs'
import { printError } from './cli.mjs'
function Group(e, name) {
@ -15,8 +12,6 @@ function Group(e, name) {
this.isExclusive = false
this.before = null
this.after = null
this.beforeEach = null
this.afterEach = null
}
Group.prototype.timeout = function(time) {
@ -64,13 +59,6 @@ function Test(e, group, name, func) {
this.name = name
this.func = func
this.error = null
this.startTime = null
this.totalTime = 0
}
Test.prototype.calculateTime = function() {
let end = process.hrtime(this.startTime)
this.totalTime = (end[0] * 1000 + Math.round(end[1] / 1000000))
}
Test.prototype.timeout = function(time) {
@ -82,42 +70,11 @@ Test.prototype.skip = function() {
}
Test.prototype.only = function() {
if (!this.e.ignoreOnly) {
this.isExclusive = true
this.group.__hasonly(true)
}
this.isExclusive = true
this.group.__hasonly(true)
}
Test.prototype.clone = function(prefix = '') {
var t = new Test(this.e, this.group, prefix + this.name, this.func)
let properties = ['skipTest', 'isExclusive', 'customTimeout', 'isBasic', 'error']
for (let key of properties) {
t[key] = this[key]
}
return t
}
let stack = []
function captureUnknownErrors(e) {
stack.push(e)
}
function cancelCaptureUnknown(e) {
stack.splice(stack.indexOf(e), 1)
}
process.on('uncaughtException', function(err) {
for (let i = stack.length - 1; i >= 0; i--) {
if (stack[i].captureOutsideExceptions) {
stack[i].captureOutsideExceptions(err)
return
}
}
console.error('-- UNCAUGHT EXCPEPTION OUTSIDE OF TEST RUNNER --')
console.error(err)
})
function Eltro() {
this.process = process
this.__timeout = 2000
this.hasExclusive = false
this.reporter = 'list'
@ -127,9 +84,7 @@ function Eltro() {
this.activeGroup = null
this.failedTests = []
this.hasTests = false
this.starting = null
this.ignoreOnly = false
this.logger = null
this.starting = false
this.filename = ''
this.prefix = ''
this.temporary = {
@ -142,39 +97,27 @@ function Eltro() {
skip: false,
only: false
}
this.captureOutsideExceptions = null
}
Eltro.prototype.begin = function() {
if (this.starting) {
console.warn('WARNING: Multiple calls to Eltro.begin were done.')
console.warn(this.starting)
console.warn(new Error('Second call'))
return
}
this.hasTests = false
this.starting = new Error('First call')
this.starting = true
this.filename = ''
this.prefix = ''
this.fileGroupMap.clear()
}
Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child = null) {
if (this.reporter === 'list' && prefix === 'Test') {
this.process.stdout.write(' \x1b[90m? ' + test.name + '\x1b[0m')
Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test') {
if (this.reporter === 'list') {
process.stdout.write(' \x1b[90m? ' + test.name + '\x1b[0m')
}
let markRealTest = child || test
if (!test.skipTest) {
let err = await new Promise((resolve, reject) => {
if (test.error) {
return reject(test.error)
}
if (!test.func) {
return reject(new Error(`Test ${test.name} was missing function`))
}
this.captureOutsideExceptions = reject
await new Promise((resolve, reject) => {
// Flag to check if we finished
let finished = false
let timeout = test.customTimeout || this.__timeout
@ -182,8 +125,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// Timeout timer in case test times out
let timer = setTimeout(function() {
if (finished === true) return
test.calculateTime()
reject(new Error('timeout of ' + timeout + 'ms exceeded. Ensure the done() callback is being called in this test.'))
}, timeout)
@ -193,11 +134,17 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
let checkIsCallback = (test.func.toString()).match(/^(function)? *\([^\)]+\)/)
let promise
test.startTime = process.hrtime()
// If the test requires callback, wrap it in a promise where callback
// either resolves or rejects that promise
if (checkIsCallback) {
promise = runWithCallbackSafe(test)
promise = new Promise(function(res, rej) {
test.func(function(err) {
if (err) {
return rej(err)
}
res()
})
})
} else {
// Function doesn't require a callback, run it directly
promise = test.func()
@ -211,7 +158,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
finished = true
clearTimeout(timer)
resolve()
@ -219,7 +165,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
finished = true
clearTimeout(timer)
reject(err)
@ -228,7 +173,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
// Possible this was a synchronous test, pass immediately
finished = true
clearTimeout(timer)
@ -238,7 +182,6 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
// check if our test had already finished and if so, do nothing
if (finished === true) return
test.calculateTime()
// An error occured while running function. Possible exception
// during a synchronous test or something else.
finished = true
@ -250,8 +193,7 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
if (prefix === 'Test') {
stats.passed++
}
return null
}, (err) => {
}, function(err) {
let saveError = err
if (!saveError) {
saveError = new Error(prefix + ' promise rejected with empty message')
@ -263,41 +205,35 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
}
saveError.originalError = err
}
return saveError
test.error = saveError
stats.failed++
}
)
if (err) {
markRealTest.error = err
this.failedTests.push(markRealTest.clone(child ? prefix : ''))
stats.failed++
}
} else {
stats.skipped++
}
this.captureOutsideExceptions = null
if (test.error) {
this.failedTests.push(test)
}
if (this.reporter === 'list') {
readline.clearLine(this.process.stdout, 0)
readline.cursorTo(this.process.stdout, 0, null)
if (markRealTest.skipTest) {
this.process.stdout.write(' \x1b[94m- ' + markRealTest.name + '\x1b[0m\n')
} else if (!markRealTest.error) {
if (!test.name.startsWith('~')) {
this.process.stdout.write(' \x1b[32m√\x1b[90m ' + markRealTest.name + ' (' + markRealTest.totalTime + 'ms)\x1b[0m\n')
}
process.stdout.clearLine();
process.stdout.cursorTo(0);
if (test.skipTest) {
process.stdout.write(' \x1b[94m- ' + test.name + '\x1b[0m\n')
} else if (!test.error) {
process.stdout.write(' \x1b[32m√\x1b[90m ' + test.name + '\x1b[0m\n')
} else if (prefix === 'Test') {
this.process.stdout.write(' \x1b[31m' + this.failedTests.length + ') ' + markRealTest.name + ' (' + markRealTest.totalTime + 'ms)\x1b[0m\n')
process.stdout.write(' \x1b[31m' + this.failedTests.length + ') ' + test.name + '\x1b[0m\n')
}
} else if (this.reporter === 'dot') {
if (markRealTest.skipTest) {
this.process.stdout.write('\x1b[94m.\x1b[0m')
} else if (markRealTest.error) {
this.process.stdout.write('\x1b[31m.\x1b[0m')
if (test.skipTest) {
process.stdout.write('\x1b[94m.\x1b[0m')
} else if (!test.error) {
process.stdout.write('\x1b[32m.\x1b[0m')
} else if (prefix === 'Test') {
this.process.stdout.write('\x1b[32m.\x1b[0m')
process.stdout.write('\x1b[31m.\x1b[0m')
}
}
}
@ -305,59 +241,33 @@ Eltro.prototype.__runTest = async function(stats, test, prefix = 'Test', child =
Eltro.prototype.__runGroup = async function(g, stats) {
if (g.tests.length) {
if (this.reporter === 'list') {
this.process.stdout.write(' ' + g.name + '\n')
console.log(' ' + g.name)
}
}
if (g.before) {
for (let i = 0; i < g.before.length; i++) {
await this.__runTest(stats, g.before[i], 'Before')
if (g.before[i].error) return
}
await this.__runTest(stats, g.before, 'Before')
if (g.before.error) return
}
for (let x = 0; x < g.tests.length; x++) {
if (!g.tests[x].skipTest && g.tests[x].isExclusive === g.hasExclusive) {
if (g.beforeEach) {
for (let i = 0; i < g.beforeEach.length && !g.tests[x].error; i++) {
await this.__runTest(stats, g.beforeEach[i], 'Before each: ', g.tests[x])
}
if (!g.tests[x].error) {
await this.__runTest(stats, g.tests[x])
}
} else {
await this.__runTest(stats, g.tests[x])
}
if (g.afterEach) {
let oldError = g.tests[x].error
g.tests[x].error = null
for (let i = 0; i < g.afterEach.length && !g.tests[x].error; i++) {
await this.__runTest(stats, g.afterEach[i], 'After each: ', g.tests[x])
}
if (oldError) {
g.tests[x].error = oldError
}
}
await this.__runTest(stats, g.tests[x])
}
}
for (let x = 0; x < g.groups.length; x++) {
if (!g.groups[x].skipTest && g.hasExclusive === (g.groups[x].hasExclusive || g.groups[x].isExclusive)) {
await this.__runGroup(g.groups[x], stats)
}
if (!g.groups[x].skipTest && g.hasExclusive === (g.groups[x].hasExclusive || g.groups[x].isExclusive))
await this.__runGroup(g.groups[x], stats)
}
if (g.after) {
for (let i = 0; i < g.after.length && !g.after.error; i++) {
await this.__runTest(stats, g.after[i], 'After')
}
await this.__runTest(stats, g.after, 'After')
}
}
Eltro.prototype.run = async function() {
if (this.reporter && this.reporter !== 'test') {
this.process.stdout.write('' + '\n')
this.process.stdout.write('' + '\n')
if (this.reporter) {
console.log('')
console.log('')
}
captureUnknownErrors(this)
let stats = {
passed: 0,
failed: 0,
@ -370,35 +280,27 @@ Eltro.prototype.run = async function() {
await this.__runGroup(this.groups[i], stats)
}
}
let end = process.hrtime(start)
cancelCaptureUnknown(this)
if (this.reporter === 'test') {
if (this.logger && this.logger.log) {
for (let x = 0; x < this.failedTests.length; x++) {
let test = this.failedTests[x];
this.logger.log(test.name, test.error)
}
}
} else if (this.reporter) {
this.process.stdout.write('' + '\n')
this.process.stdout.write('' + '\n')
if (this.reporter) {
console.log('')
console.log('')
if (stats.passed) {
this.process.stdout.write(' \x1b[32m' + stats.passed + ' passing \x1b[90m(' + (end[0] * 1000 + Math.round(end[1] / 1000000)) + 'ms)\x1b[0m' + '\n')
console.log(' \x1b[32m' + stats.passed + ' passing \x1b[90m(' + (end[0] * 1000 + Math.round(end[1] / 1000000)) + 'ms)\x1b[0m')
}
if (stats.failed) {
this.process.stdout.write(' \x1b[31m' + stats.failed + ' failing\x1b[0m' + '\n')
console.log(' \x1b[31m' + stats.failed + ' failing\x1b[0m')
}
if (stats.skipped) {
this.process.stdout.write(' \x1b[94m' + stats.skipped + ' pending\x1b[0m' + '\n')
console.log(' \x1b[94m' + stats.skipped + ' pending\x1b[0m')
}
this.process.stdout.write('' + '\n')
console.log('')
if (this.failedTests.length) {
for (let x = 0; x < this.failedTests.length; x++) {
let test = this.failedTests[x];
this.process.stdout.write(' ' + (x + 1) + ') ' + test.name + ':' + '\n')
console.log(' ' + (x + 1) + ') ' + test.name + ':')
printError(test.error)
}
}
@ -419,48 +321,37 @@ Eltro.prototype.resetFilename = function() {
this.activeGroup = null
}
let beforesandafters = [
['before', '~Before', false],
['after', '~After', false],
['beforeEach', '~Before each', true],
['afterEach', '~After each', true],
]
beforesandafters.forEach(function(item) {
let beforeAfter = item[0]
let fullName = item[1]
let bringToChildren = item[2]
Eltro.prototype[beforeAfter] = function(func) {
if (!this.activeGroup) {
throw new Error('Tests outside groups are not allowed.')
}
let test = func
if (!(test instanceof Test)) {
test = new Test(this, this.activeGroup, fullName + ': ' + this.activeGroup.name, func)
}
if (this.temporary.timeout || this.activeGroup.customTimeout) {
test.timeout(this.temporary.timeout || this.activeGroup.customTimeout)
this.temporary.timeout = 0
}
this.activeGroup[beforeAfter] = this.activeGroup[beforeAfter] || []
this.activeGroup[beforeAfter].push(test)
if (bringToChildren) {
for (let group of this.activeGroup.groups) {
group[beforeAfter].push(test)
}
}
return test
Eltro.prototype.before = function(func) {
if (!this.activeGroup) {
throw new Error('Tests outside groups are not allowed.')
}
})
let bringToChildren = ['beforeEach', 'afterEach']
let test = new Test(this, this.activeGroup, 'Before: ' + this.activeGroup.name, func)
if (this.temporary.timeout || this.activeGroup.customTimeout) {
test.timeout(this.temporary.timeout || this.activeGroup.customTimeout)
this.temporary.timeout = 0
}
this.activeGroup.before = test
return test
}
Eltro.prototype.after = function(func) {
if (!this.activeGroup) {
throw new Error('Tests outside groups are not allowed.')
}
let test = new Test(this, this.activeGroup, 'After: ' + this.activeGroup.name, func)
if (this.temporary.timeout || this.activeGroup.customTimeout) {
test.timeout(this.temporary.timeout || this.activeGroup.customTimeout)
this.temporary.timeout = 0
}
this.activeGroup.after = test
return test
}
Eltro.prototype.describe = function(name, func) {
let before = this.activeGroup
@ -490,16 +381,6 @@ Eltro.prototype.describe = function(name, func) {
this.temporary.only = false
}
if (before) {
for (let beforeAfter of bringToChildren) {
if (!before[beforeAfter]) continue
for (let test of before[beforeAfter]) {
this[beforeAfter](test)
}
}
}
func()
this.activeGroup = before
@ -516,9 +397,7 @@ Eltro.prototype.skip = function() {
}
Eltro.prototype.only = function() {
if (!this.ignoreOnly) {
this.temporary.only = true
}
this.temporary.only = true
return this
}
@ -527,18 +406,9 @@ Eltro.prototype.test = function(name, func) {
throw new Error('Tests outside groups are not allowed.')
}
let test = new Test(
this,
this.activeGroup,
[this.activeGroup.name.trim(), (name || '').trim()].filter(x => x).join(' '),
func
)
let test = new Test(this, this.activeGroup, this.activeGroup.name + ' ' + name, func)
this.activeGroup.tests.push(test)
if (name == null) {
test.error = new Error(`An empty test or missing name under ${this.activeGroup.name.trim()} was found`)
}
if (this.temporary.only && !this.temporary.skip) {
test.only()
this.temporary.only = false

View file

@ -1,85 +0,0 @@
import { promisify } from 'util'
import { spawn, exec } from 'child_process'
const execPromise = promisify(exec)
export default function kill(pid, signal) {
let pids = new Set([pid])
let getSpawn = null
let getPids = null
switch (process.platform) {
case 'win32':
return execPromise('taskkill /pid ' + pid + ' /T /F').then(() => pids)
case 'darwin':
getSpawn = function(parentPid) {
return spawn('pgrep', ['-P', parentPid])
}
getPids = function(data) {
return data.match(/\d+/g).map(Number)
}
break
default:
getSpawn = function (parentPid) {
return exec('ps -opid="" -oppid="" | grep ' + parentPid)
}
getPids = function(data, parentPid) {
let output = data.trim().split('\n')
return output.map(line => {
let [child, parent] = line.trim().split(/ +/)
if (Number(parent) === parentPid) {
return Number(child)
}
return 0
}).filter(x => x)
}
break
}
return buildTree(pids, getSpawn, getPids, pid)
.then(function() {
for (let pid of pids) {
try {
process.kill(pid, signal)
} catch (err) {
if (err.code !== 'ESRCH') throw err;
}
}
return pids
})
}
function buildTree(allPids, spawnGetChildren, spawnGetPids, parentPid) {
allPids.add(parentPid)
let ps = spawnGetChildren(parentPid)
let data = ''
let err = ''
ps.stdout.on('data', function(buf) {
data += buf.toString('ascii')
})
ps.stderr.on('data', function(buf) {
err += buf.toString('ascii')
})
return new Promise(function(res, rej) {
ps.on('close', function(code) {
// Check if ps errored out
if (code !== 0 && err.trim()) {
return rej(new Error('Error running ps to kill processes:\n\t' + err))
}
// Check if we otherwise got an error code (usually means empty results)
if (code !== 0 || !data.trim()) return res()
let pids = spawnGetPids(data, parentPid)
res(Promise.all(
pids.filter(pid => pid && !allPids.has(pid))
.map(buildTree.bind(this, allPids, spawnGetChildren, spawnGetPids))
))
})
})
}

View file

@ -12,8 +12,6 @@ export function stub(returnFunc = null) {
if (returnFunc && typeof(returnFunc) !== 'function') {
throw new Error('stub() was called with non-function argument')
}
let manualReturners = new Map()
let nextManual = null
let returner = returnFunc ? returnFunc : null
let calls = []
let func = function(...args) {
@ -24,9 +22,6 @@ export function stub(returnFunc = null) {
func[indexMap[func.callCount]] = args
}
func.callCount++
if (manualReturners.has(func.callCount -1)) {
return manualReturners.get(func.callCount -1)(...args)
}
if (returner) {
return returner(...args)
}
@ -34,71 +29,36 @@ export function stub(returnFunc = null) {
func.lastCall = null
func.called = false
func.callCount = 0
func.calls = calls
func.findCall = function(fn) {
for (let call of calls) {
if (fn(call)) return call
}
return null
}
func.getCall = function(i) {
func.onCall = function(i) {
return calls[i]
}
func.getCallN = function(i) {
return calls[i - 1]
}
func.onCall = function(i) {
if (i !== null && typeof(i) !== 'number') {
throw new Error('onCall must be called with either null or number')
}
nextManual = i
return func
}
func.onCallN = function(i) {
return func.onCall(i - 1)
}
func.reset = function() {
func.lastCall = null
func.called = false
func.callCount = 0
manualReturners.clear()
for (let i = 0; i < indexMap.length; i++) {
func[indexMap[i]] = null
}
returner = returnFunc ? returnFunc : null
calls.splice(0, calls.length)
return func
}
func.returns = function(data) {
func.returnWith(function() { return data })
return func
}
func.throws = function(data) {
func.returnWith(function() { throw data })
return func
}
func.resolves = function(data) {
func.returnWith(function() { return Promise.resolve(data) })
return func
}
func.rejects = function(data) {
func.returnWith(function() { return Promise.reject(data) })
return func
returner = function() { return data }
}
func.returnWith = function(returnFunc) {
if (typeof(returnFunc) !== 'function') {
throw new Error('stub() was called with non-function argument')
}
if (nextManual !== null) {
manualReturners.set(nextManual, returnFunc)
nextManual = null
} else {
returner = returnFunc
}
return func
returner = returnFunc
}
func.throws = function(data) {
returner = function() { throw data }
}
func.resolves = function(data) {
returner = function() { return Promise.resolve(data) }
}
func.rejects = function(data) {
returner = function() { return Promise.reject(data) }
}
for (let i = 0; i < indexMap.length; i++) {
func[indexMap[i]] = null

View file

@ -1,22 +0,0 @@
(The MIT License)
Copyright (c) 2012-2021 Yuan Chuan <yuanchuan23@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,118 +0,0 @@
import fs from 'fs'
import os from 'os'
import path from 'path'
import * as is from './is.mjs'
let IS_SUPPORT
let TEMP_DIR = os.tmpdir && os.tmpdir()
|| process.env.TMPDIR
|| process.env.TEMP
|| process.cwd()
function TempStack() {
this.stack = []
}
TempStack.prototype.create = function(type, base) {
let name = path.join(base,
'node-watch-' + Math.random().toString(16).substr(2)
)
this.stack.push({ name: name, type: type })
return name
}
TempStack.prototype.write = function(/* file */) {
for (let i = 0; i < arguments.length; ++i) {
fs.writeFileSync(arguments[i], ' ')
}
}
TempStack.prototype.mkdir = function(/* dirs */) {
for (let i = 0; i < arguments.length; ++i) {
fs.mkdirSync(arguments[i])
}
}
TempStack.prototype.cleanup = function(fn) {
try {
let temp
while ((temp = this.stack.pop())) {
let type = temp.type
let name = temp.name
if (type === 'file' && is.file(name)) {
fs.unlinkSync(name)
}
else if (type === 'dir' && is.directory(name)) {
fs.rmdirSync(name)
}
}
}
finally {
if (is.func(fn)) fn()
}
}
let pending = false
export default function hasNativeRecursive(fn, opts = {}) {
if (!is.func(fn)) {
return false
}
if (IS_SUPPORT !== undefined) {
return fn(IS_SUPPORT)
}
if (opts.quickCheck) {
return fn(IS_SUPPORT = (process.platform === 'darwin' || process.platform === 'win32'))
}
if (!pending) {
pending = true
}
// check again later
else {
return setTimeout(function() {
hasNativeRecursive(fn)
}, 300)
}
let stack = new TempStack()
let parent = stack.create('dir', TEMP_DIR)
let child = stack.create('dir', parent)
let file = stack.create('file', child)
stack.mkdir(parent, child)
let options = { recursive: true }
let watcher
try {
watcher = fs.watch(parent, options)
} catch (e) {
if (e.code == 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') {
return fn(IS_SUPPORT = false)
} else {
throw e
}
}
if (!watcher) {
return false
}
let timer = setTimeout(function() {
watcher.close()
stack.cleanup(function() {
fn(IS_SUPPORT = false)
})
}, 200)
watcher.on('change', function(evt, name) {
if (path.basename(file) === path.basename(name)) {
watcher.close()
clearTimeout(timer)
stack.cleanup(function() {
fn(IS_SUPPORT = true)
})
}
})
stack.write(file)
}

View file

@ -1,303 +0,0 @@
import fs from 'fs'
import fsPromise from 'fs/promises'
import path from 'path'
import events from 'events'
import hasNativeRecursive from './has-native-recursive.mjs'
import * as is from './is.mjs'
export const EVENT_UPDATE = 'update';
export const EVENT_REMOVE = 'remove';
const TYPE_FILE = 'file'
const TYPE_DIRECTORY = 'directory'
function unique(arr) {
return arr.filter(function(v, i, self) {
return self.indexOf(v) === i;
});
}
export default class Watcher extends events.EventEmitter {
constructor(path, options = null, fn = null, { fs: fsoverwrite } = {}) {
super()
this.ac = new AbortController()
events.setMaxListeners(2000, this.ac.signal)
this._fs = fsoverwrite || fs
this._cache = []
this._cacheTimeout = null
this.listeners = []
this.closed = false
let paths = path
if (is.buffer(paths)) {
paths = paths.toString()
}
if (!is.array(paths)) {
paths = [paths]
}
paths = unique(paths)
this.options = options || {}
this.fn = fn || null
this.originalPaths = paths
if (is.func(this.options)) {
this.fn = this.options
this.options = {}
}
this._verifyOptions(paths)
}
isClosed() {
return this.closed
}
close() {
this.closed = true
this.ac.abort()
this._cache = this.listeners = []
this.emitAsync('close')
}
emitAsync(name, ...args) {
process.nextTick(() => this.emit(name, ...args))
}
_verifyOptions(paths) {
for (let path of paths) {
if (!is.exists(path)) {
this.emitAsync('error', new Error(path + ' does not exist.'))
}
}
if (this.options.encoding) {
if (this.options.encoding && this.options.encoding !== 'buffer' && !Buffer.isEncoding(this.options.encoding)) {
throw new Error('Unknown encoding: ' + this.options.encoding);
}
} else {
this.options.encoding = 'utf8'
}
if (this.options.delay !== 0 && !this.options.delay) {
this.options.delay = 200
}
if (is.func(this.fn)) {
this.on('change', this.fn)
}
if (this.options.manualRecursive !== true) {
hasNativeRecursive(nativeRecursive => {
this.supportsNativeRecursive = nativeRecursive
this.options.manualRecursive = !nativeRecursive
this._startListeners(paths)
}, { quickCheck: this.options.quickNativeCheck || true })
} else {
this._startListeners(paths)
}
}
_startListeners(paths) {
Promise.all(paths.map(path => this.safeAdd(path)))
.then(
() => this.emit('ready'),
err => this.emit('error', err),
)
}
getWatcherOrNull(name) {
for (let check of this.listeners) {
if (check.path === name) {
return check
}
}
return null
}
shouldSkip(name) {
return this.options.skip
?
(is.func(this.options.skip) && this.options.skip.call(this, name))
|| (is.regExp(this.options.skip) && this.options.skip.test(name))
: false
}
shouldNotify(name) {
return this.options.filter
?
(is.func(this.options.filter) && this.options.filter.call(this, name))
|| (is.regExp(this.options.filter) && this.options.filter.test(name))
: true
}
closeWatch(orgItem) {
let item = orgItem
if (typeof item === 'string') {
item = this.getWatcherOrNull(item)
}
if (!item) {
return
}
if (item.watcher) {
item.watcher.close()
}
this._emitEvent(item, EVENT_REMOVE, item.path)
let index = this.listeners.indexOf(item)
if (index < 0) return
this.listeners.splice(index, 1)
}
_emitEvent(item, evt, name) {
if (item.type === TYPE_FILE && !is.samePath(name, item.filename)) return
if (item.type === TYPE_DIRECTORY && this.shouldSkip(name)) return
if (!this.shouldNotify(name)) return
if (item.flag) {
item.flag = ''
return
}
let outputName = name
if (this.options.encoding !== 'utf8') {
outputName = Buffer.from(outputName)
if (this.options.encoding !== 'buffer') {
outputName = outputName.toString(this.options.encoding)
}
}
if (!this.options.delay) {
this.emit('change', evt, outputName)
this.emit('changed', evt, outputName)
return
}
this._cache.push([evt, name, outputName])
if (this._cacheTimeout) return
this._cacheTimeout = setTimeout(() => {
let cache = this._filterCache(this._cache)
this._cache = []
this._cacheTimeout = null
for (let event of cache) {
try {
this.emit('change', event[0], event[2])
} catch (err) {
this.emit('error', err)
}
}
this.emit('changed')
}, this.options.delay)
}
_filterCache(cache) {
let setFound = new Set()
let out = cache.reverse().filter(([evt, name]) => {
if (setFound.has(name)) return false
setFound.add(name)
return true
}).reverse()
return out
}
_watcherSink(item, rawEvt, rawName, c) {
if (this.closed) return
let name = path.join(item.path, rawName || '')
let evt = is.exists(name) ? EVENT_UPDATE : EVENT_REMOVE
if (this.options.recursive && this.options.manualRecursive && item.type === TYPE_DIRECTORY) {
if (evt === EVENT_REMOVE) {
this.closeWatch(name)
return
} else {
if (is.directory(name)
&& this.getWatcherOrNull(name) === null) {
this.safeAdd(name, TYPE_DIRECTORY)
}
}
}
this._emitEvent(item, evt, name)
}
_pathToItem(name, type) {
if (type === TYPE_FILE) {
let parent = path.join(name, '../')
return {
path: parent,
type: TYPE_FILE,
filename: name,
watcher: null,
flag: '',
}
} else {
return {
path: name,
type: TYPE_DIRECTORY,
watcher: null,
flag: '',
}
}
}
_watcherError(item, err) {
if (this.closed) return
if (is.windows() && err.code === 'EPERM') {
this.closeWatch(item)
item.flag = 'windows-error'
} else {
self.emit('error', err)
}
}
safeAdd(name, orgType) {
let type = orgType
if (!type) {
type = is.file(name) ? TYPE_FILE : TYPE_DIRECTORY
}
if (this.shouldSkip(name)) {
return
}
let item = this._pathToItem(name, type)
let options = {
encoding: 'utf8',
signal: this.ac.signal,
}
if (!this.options.manualRecursive && item.type !== TYPE_FILE && this.options.recursive) {
options.recursive = true
}
try {
item.watcher = this._fs.watch(item.path, options)
} catch (err) {
this.emitAsync('error', err)
}
if (!item.watcher) return
this.listeners.push(item)
item.watcher.on('error', this._watcherError.bind(this, item))
item.watcher.on('change', this._watcherSink.bind(this, item))
if (options.recursive || item.type === TYPE_FILE) return
return fsPromise.readdir(item.path, { withFileTypes: true })
.then(directories => directories.filter(dir => dir.isDirectory()))
.then(directories => {
return Promise.all(directories.map(dir => this.safeAdd(path.join(item.path, dir.name), TYPE_DIRECTORY)))
})
}
}

View file

@ -1,74 +0,0 @@
import fs from 'fs'
import path from 'path'
import os from 'os'
function matchObject(item, str) {
return Object.prototype.toString.call(item)
=== '[object ' + str + ']'
}
function checkStat(name, fn) {
try {
return fn(name)
} catch (err) {
if (/^(ENOENT|EPERM|EACCES)$/.test(err.code)) {
if (err.code !== 'ENOENT') {
console.warn('Warning: Cannot access %s', name)
}
return false
}
throw err
}
}
export function nil(item) {
return item == null
}
export function array(item) {
return Array.isArray(item)
}
export function emptyObject(item) {
for (var key in item) {
return false
}
return true
}
export function buffer(item) {
return Buffer.isBuffer(item)
}
export function regExp(item) {
return matchObject(item, 'RegExp')
}
export function string(item) {
return matchObject(item, 'String')
}
export function func(item) {
return typeof item === 'function'
}
export function number(item) {
return matchObject(item, 'Number')
}
export function exists(name) {
return fs.existsSync(name)
}
export function file(name) {
return checkStat(name, function(n) {
return fs.statSync(n).isFile()
})
}
export function samePath(a, b) {
return path.resolve(a) === path.resolve(b)
}
export function directory(name) {
return checkStat(name, function(n) {
return fs.statSync(n).isDirectory()
})
}
export function symbolicLink(name) {
return checkStat(name, function(n) {
return fs.lstatSync(n).isSymbolicLink()
})
}
export function windows() {
return os.platform() === 'win32'
}

View file

@ -1,33 +1,10 @@
{
"name": "eltro",
"version": "1.6.1",
"version": "1.2.3",
"description": "Eltro is a tiny no-dependancy test framework for node",
"main": "index.mjs",
"types": "eltro.d.ts",
"scripts": {
"echo": "echo helloworld",
"echo:watch": "node cli.mjs --watch test --npm echo",
"test": "node cli.mjs \"test/**/*.test.mjs\"",
"test:watch": "node cli.mjs \"test/**/*.test.mjs\" --watch test",
"test:watch:legacy": "npm-watch test"
},
"watch": {
"test": {
"patterns": [ "lib", "test", "cli.mjs", "index.mjs" ],
"extensions": "js,mjs",
"delay": 50
},
"test_quick": {
"patterns": [ "cli.mjs", "index.mjs" ]
},
"test_quick_js": {
"patterns": [ "cli.mjs", "index.mjs" ],
"extensions": "js"
},
"test_invalid_extensions": {
"patterns": [ "cli.mjs", "index.mjs" ],
"extensions": ".js.bla"
}
"test": "node cli.mjs test/**/*.test.mjs -r dot"
},
"repository": {
"type": "git",
@ -50,7 +27,6 @@
"eltro": "./cli.mjs"
},
"files": [
"eltro.d.ts",
"index.mjs",
"cli.mjs",
"README.md",

View file

@ -29,63 +29,6 @@ t.describe('#notOk()', function() {
})
})
t.describe('#equalWithMargin()', function() {
t.test('should support default margin for floating point math', function() {
let check = 0.1 + 0.2
assertExtended.throws(function() {
assertExtended.strictEqual(check, 0.3)
}, assertExtended.AssertionError)
assertExtended.equalWithMargin(check, 0.3)
})
t.test('should support custom margin', function() {
assertExtended.equalWithMargin(1, 2, 1)
})
t.test('should fail if margin is too small', function() {
assertExtended.throws(function() {
assertExtended.equalWithMargin(1, 2, 0.9)
}, assertExtended.AssertionError)
})
t.test('should support custom message', function () {
const assertMessage = 'Hello world'
let error = null
try {
assertExtended.equalWithMargin(1, 2, 0, assertMessage)
} catch (err) {
error = err
}
assert.ok(error)
assert.match(error.message, new RegExp(assertMessage))
})
})
t.describe('#throwAndCatch()', function() {
t.test('should work and return the original error', function() {
const assertError = new Error('Speed')
let err = assert.throwsAndCatch(() => { throw assertError })
assert.strictEqual(err, assertError)
})
t.test('should otherwise throw if no error', function() {
const assertMessage = 'Hello world'
let error = null
try {
assert.throwsAndCatch(() => { }, assertMessage)
} catch (err) {
error = err
}
assert.ok(error)
assert.match(error.message, new RegExp(assertMessage))
})
})
t.describe('#isFulfilled()', function() {
t.test('should exist', function() {
assertExtended.ok(assertExtended.isFulfilled)

View file

@ -1,217 +0,0 @@
import { runWithCallbackSafe } from '../lib/callback.mjs'
import assert from '../lib/assert.mjs'
import t from '../lib/eltro.mjs'
t.describe('runCb()', function() {
t.test('cb() should work normally', async function() {
let called = false
let test = { func: function(cb) { called = true; cb() } }
await runWithCallbackSafe(test)
assert.strictEqual(called, true)
})
t.test('cb() should work with timeout', async function() {
let called = false
let test = { func: function(cb) {
setImmediate(function() {
called = true;
cb();
})
} }
await runWithCallbackSafe(test)
assert.strictEqual(called, true)
})
t.test('cb() should capture throws outside', async function() {
const assertError = new Error('a')
let test = { func: function(cb) { throw assertError } }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb() should support callback error', async function() {
const assertError = new Error('a')
let test = { func: function(cb) { cb(assertError) } }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb() should support callback error in immediate', async function() {
const assertError = new Error('a')
let test = { func: function(cb) {
setImmediate(function() {
cb(assertError)
})
} }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb.wrap() should return function and work with timeout', async function() {
let calledFirst = false
let calledSecond = false
let test = { func: function(cb) {
let fun = cb.wrap(function() {
calledSecond = true
cb()
})
setImmediate(function() {
calledFirst = true;
fun()
})
} }
await runWithCallbackSafe(test)
assert.strictEqual(calledFirst, true)
assert.strictEqual(calledSecond, true)
})
t.test('cb.wrap() should pass arguments correctly', async function() {
let a = 0
let b = 0
let c = 0
let called = false
let test = { func: function(cb) {
let fun = cb.wrap(function(ina, inb, inc) {
a = ina
b = inb
c = inc
cb()
})
setImmediate(function() {
called = true
fun(1, 2, 3)
})
} }
await runWithCallbackSafe(test)
assert.strictEqual(called, true)
assert.strictEqual(a, 1)
assert.strictEqual(b, 2)
assert.strictEqual(c, 3)
await runWithCallbackSafe(test)
})
t.test('cb.wrap() should throw', async function() {
const assertError = new Error('a')
let test = { func: function(cb) {
setImmediate(cb.wrap(function() {
throw assertError
}))
} }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb.wrap() should support nested calls', async function() {
const assertError = new Error('a')
let test = { func: function(cb) {
setImmediate(function() {
setImmediate(cb.wrap(function() {
throw assertError
}))
})
} }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb.finish() should return function and work with timeout and finish', async function() {
let calledFirst = false
let calledSecond = false
let test = { func: function(cb) {
let fun = cb.finish(function() {
calledSecond = true
})
setImmediate(function() {
calledFirst = true;
fun()
})
} }
await runWithCallbackSafe(test)
assert.strictEqual(calledFirst, true)
assert.strictEqual(calledSecond, true)
})
t.test('cb.finish() should pass arguments correctly', async function() {
let a = 0
let b = 0
let c = 0
let called = false
let test = { func: function(cb) {
let fun = cb.finish(function(ina, inb, inc) {
a = ina
b = inb
c = inc
})
setImmediate(function() {
called = true
fun(1, 2, 3)
})
} }
await runWithCallbackSafe(test)
assert.strictEqual(called, true)
assert.strictEqual(a, 1)
assert.strictEqual(b, 2)
assert.strictEqual(c, 3)
await runWithCallbackSafe(test)
})
t.test('cb.finish() should support throw', async function() {
const assertError = new Error('a')
let test = { func: function(cb) {
setImmediate(cb.finish(function() {
throw assertError
}))
} }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
t.test('cb.finish() should support nested throw calls', async function() {
const assertError = new Error('a')
let test = { func: function(cb) {
setImmediate(function() {
setImmediate(cb.finish(function() {
throw assertError
}))
})
} }
let err = await assert.isRejected(runWithCallbackSafe(test))
assert.strictEqual(err, assertError)
})
})

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -147,38 +147,6 @@ e.test('Eltro should support callback', async function() {
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support custom cb with finish wrap', async function() {
let actualRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.test('a', function(cb) {
setImmediate(() => {
setImmediate(cb.finish(() => { actualRan++; }))
})
})
t.test('b', function(cb) {
setImmediate(() => {
setImmediate(cb.finish(() => { actualRan++; }))
})
})
t.test('c', function(cb) {
setImmediate(() => {
setImmediate(cb.finish(() => { actualRan++; }))
})
})
t.test('d', function(cb) {
setImmediate(() => {
setImmediate(cb.finish(() => { actualRan++; }))
})
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(actualRan, 4)
})
e.test('Eltro should support directly thrown errors', async function() {
testsWereRun = true
const assertError = new Error()
@ -226,58 +194,6 @@ e.test('Eltro should support callback rejected errors', async function() {
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support capturing unknown errors outside scope', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.test('', function(cb) {
process.nextTick(function() {
throw assertError
})
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should log an error if test is missing', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.describe('herpderp', function() {
t.test('blatest')
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.match(t.failedTests[0].error.message, /herpderp/)
assert.match(t.failedTests[0].error.message, /blatest/)
assert.match(t.failedTests[0].error.message, /missing/)
})
e.test('Eltro should log an error if text is missing', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.describe('herpderp', function() {
t.test()
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.match(t.failedTests[0].error.message, /herpderp/)
assert.match(t.failedTests[0].error.message, /empty/)
assert.match(t.failedTests[0].error.message, /name/)
})
e.test('Eltro should support timing out tests', async function() {
testsWereRun = true
const t = CreateT()
@ -352,35 +268,6 @@ e.test('Eltro should support only tests', async function() {
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should ignore only if ignoreOnly is specified', async function() {
testsWereRun = true
let assertIsRun = 0
const t = CreateT()
t.ignoreOnly = true
t.begin()
t.describe('', function() {
t.test('a', function() { assertIsRun++ })
t.test('b', function() { assertIsRun++ }).only()
t.describe('sub', function() {
t.test('d', function() { assertIsRun++ })
t.only().test('c', function() { assertIsRun++ })
})
t.only().describe('sub2', function() {
t.test('d', function() { assertIsRun++ })
t.test('c', function() { assertIsRun++ })
})
t.describe('sub3', function() {
t.test('d', function() { assertIsRun++ })
t.test('c', function() { assertIsRun++ })
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertIsRun, 8)
})
e.test('Eltro should support skipped tests in front of the test', async function() {
testsWereRun = true
let assertIsTrue = false
@ -396,6 +283,380 @@ e.test('Eltro should support skipped tests in front of the test', async function
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support before() functions in describe group', async function() {
testsWereRun = true
let assertRan = 0
let firstBefore = 0
let secondBefore = 0
let thirdBefore = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function() {
firstBefore = assertRan
})
t.describe('', function() {
t.before(function() {
secondBefore = assertRan
})
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.before(function() {
thirdBefore = assertRan
})
t.test('', function() { assertRan++ })
})
t.test('', function() { assertRan++ })
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertRan, 5)
assert.strictEqual(stats.passed, 5)
assert.strictEqual(firstBefore, 0)
assert.strictEqual(secondBefore, 1)
assert.strictEqual(thirdBefore, 4)
})
e.test('Eltro should support before() functions in describe, timing out', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function(cb) { }).timeout(50)
t.test('', function() { assertRan++ })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /50ms/)
assert.strictEqual(assertRan, 0)
})
e.test('Eltro should support before() functions in describe, late timing out', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function(cb) {
setTimeout(cb, 100)
}).timeout(50)
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /50ms/)
assert.strictEqual(assertRan, 2)
})
e.test('Eltro should support before() functions in describe, timing out in front', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.timeout(25).before(function(cb) { setTimeout(cb, 50) })
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /25ms/)
assert.strictEqual(assertRan, 2)
})
e.test('Eltro should support before() functions in describe, being promised', async function() {
testsWereRun = true
let assertIsTrue = false
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function() {
return new Promise(function(res) {
assertIsTrue = true
res()
})
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support before() functions in describe, support callback', async function() {
testsWereRun = true
let assertIsTrue = false
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function(cb) {
setTimeout(function() {
assertIsTrue = true
cb()
}, 25)
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support before() functions in describe, support directly thrown errors', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function() {
throw assertError
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support before() functions in describe, support rejected promises', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function() {
return new Promise(function(res, rej) {
rej(assertError)
})
})
t.test('', function() {})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support before() functions in describe, support callback rejected', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.before(function(cb) { cb(assertError) })
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support after() functions in describe group', async function() {
testsWereRun = true
let assertRan = 0
let firstAfter = 0
let secondAfter = 0
let thirdAfter = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function() {
firstAfter = assertRan
})
t.describe('', function() {
t.after(function() {
secondAfter = assertRan
})
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.after(function() {
thirdAfter = assertRan
})
t.test('', function() { assertRan++ })
})
t.test('', function() { assertRan++ })
})
let stats = await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(stats.passed, 5)
assert.strictEqual(assertRan, 5)
assert.strictEqual(firstAfter, 5)
assert.strictEqual(secondAfter, 4)
assert.strictEqual(thirdAfter, 5)
})
e.test('Eltro should support after() functions in describe, timing out', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function(cb) { }).timeout(50)
t.test('', function() { assertRan++ })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /50ms/)
assert.strictEqual(assertRan, 1)
})
e.test('Eltro should support after() functions in describe, late timing out', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function(cb) {
setTimeout(cb, 100)
}).timeout(50)
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /50ms/)
assert.strictEqual(assertRan, 3)
})
e.test('Eltro should support after() functions in describe, timing out in front', async function() {
testsWereRun = true
let assertRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.timeout(25).after(function(cb) { setTimeout(cb, 50) })
t.test('', function() { assertRan++ })
})
t.describe('', function() {
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
t.test('', function(cb) { assertRan++; setTimeout(cb, 25) })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.ok(t.failedTests[0].error)
assert.match(t.failedTests[0].error.message, /25ms/)
assert.strictEqual(assertRan, 3)
})
e.test('Eltro should support after() functions in describe, being promised', async function() {
testsWereRun = true
let assertIsTrue = false
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function() {
return new Promise(function(res) {
assertIsTrue = true
res()
})
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support after() functions in describe, support callback', async function() {
testsWereRun = true
let assertIsTrue = false
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function(cb) {
setTimeout(function() {
assertIsTrue = true
cb()
}, 25)
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 0)
assert.strictEqual(assertIsTrue, true)
})
e.test('Eltro should support after() functions in describe, support directly thrown errors', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function() {
throw assertError
})
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support after() functions in describe, support rejected promises', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function() {
return new Promise(function(res, rej) {
rej(assertError)
})
})
t.test('', function() {})
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support after() functions in describe, support callback rejected', async function() {
testsWereRun = true
const assertError = new Error()
const t = CreateT()
t.begin()
t.describe('', function() {
t.after(function(cb) { cb(assertError) })
t.test('', function() { })
})
await t.run()
assert.strictEqual(t.failedTests.length, 1)
assert.strictEqual(t.failedTests[0].error, assertError)
})
e.test('Eltro should support only tests in front of the test', async function() {
testsWereRun = true
let assertIsTrue = false
@ -593,8 +854,9 @@ e.test('Eltro nested timeout should work as expected', async function() {
// Extra testing to make sure tests were run at all
process.on('exit', function(e) {
try {
assert.strictEqual(testsWereRun, true, 'Not all tests were run, remove all .only() and try again.')
assert.strictEqual(testsWereRun, true)
} catch(err) {
console.log('Checking if tests were run at all failed:')
printError(err)
process.exit(1)
}

View file

@ -49,41 +49,3 @@ e.test('Eltro should support any value in throws', async function() {
assert.ok(t.failedTests[x].error.stack)
}
})
e.test('Eltro should support custom cb with safe wrap', async function() {
let actualRan = 0
const t = CreateT()
t.begin()
t.describe('', function() {
t.test('a', function(cb) {
setImmediate(() => {
setImmediate(cb.wrap(() => { actualRan++; throw null }))
})
})
t.test('b', function(cb) {
setImmediate(() => {
setImmediate(cb.wrap(() => { actualRan++; throw {} }))
})
})
t.test('c', function(cb) {
setImmediate(() => {
setImmediate(cb.wrap(() => { actualRan++; throw { message: 'test' } }))
})
})
t.test('d', function(cb) {
setImmediate(() => {
setImmediate(cb.wrap(() => { actualRan++; throw 1234 }))
})
})
})
await t.run()
assert.strictEqual(t.failedTests.length, 4)
for (let x = 0; x < t.failedTests.length; x++) {
assert.strictEqual(typeof(t.failedTests[x].error), 'object')
assert.ok(t.failedTests[x].error.message)
assert.ok(t.failedTests[x].error.stack)
}
assert.strictEqual(actualRan, t.failedTests.length)
})

View file

@ -1,41 +0,0 @@
import { spawn } from 'child_process'
import t from '../../lib/eltro.mjs'
import assert from '../../lib/assert.mjs'
import kill from '../../lib/kill.mjs'
t.describe('kill', function() {
let worker
t.afterEach(function() {
if (worker?.pid && !worker.killed) {
worker.kill()
}
})
t.test('should kill process correctly', function(done) {
worker = spawn('node', ['./test/kill/runner.mjs'])
assert.ok(worker.pid)
worker.on('exit', done.finish(function(code, signal) {
assert.ok(code || signal)
}))
kill(worker.pid)
})
t.test('should succeed in killing tree', async function() {
worker = spawn('node', ['./test/kill/runner.mjs'])
assert.ok(worker.pid)
// Give it some time to start
await new Promise(res => {
worker.stdout.on('data', function(data) {
if (data.toString().indexOf('secondary') >= 0) res()
})
})
return kill(worker.pid).then(function(pids) {
assert.strictEqual(pids.size, 2)
})
})
})

View file

@ -1,11 +0,0 @@
import { spawn } from 'child_process'
console.log('primary', process.pid)
let secondary = spawn('node', ['./test/kill/second_runner.mjs'])
secondary.stdout.on('data', function(data) {
process.stdout.write(data)
})
setInterval(function() { console.log('primary', process.pid) }, 100)

View file

@ -1,2 +0,0 @@
console.log('secondary', process.pid)
setInterval(function() { console.log('secondary', process.pid) }, 100)

View file

@ -86,45 +86,16 @@ import { spy, stub } from '../index.mjs'
assert.strictEqual(spyer.lastCall[1], assertThirdArgs[1])
assert.strictEqual(spyer.callCount, 3)
assert.strictEqual(spyer.getCall(0)[0], assertFirstArgs)
assert.strictEqual(spyer.getCall(1)[0], assertSecondArgs)
assert.strictEqual(spyer.getCall(2)[0], assertThirdArgs[0])
assert.strictEqual(spyer.getCall(2)[1], assertThirdArgs[1])
assert.strictEqual(spyer.onCall(0)[0], assertFirstArgs)
assert.strictEqual(spyer.onCall(1)[0], assertSecondArgs)
assert.strictEqual(spyer.onCall(2)[0], assertThirdArgs[0])
assert.strictEqual(spyer.onCall(2)[1], assertThirdArgs[1])
assert.strictEqual(spyer.firstCall[0], assertFirstArgs)
assert.strictEqual(spyer.secondCall[0], assertSecondArgs)
assert.strictEqual(spyer.thirdCall[0], assertThirdArgs[0])
assert.strictEqual(spyer.thirdCall[1], assertThirdArgs[1])
})
t.test('should support searching for a call', function() {
const assertFirstArgs = 'asdf'
const assertSecondArgs = { a: 1 }
const assertThirdArgs = [{ b: 1 }, { c: 2 }]
let spyer = tester()
assert.notOk(spyer.called)
spyer(assertFirstArgs)
spyer(assertSecondArgs)
spyer(assertThirdArgs[0], assertThirdArgs[1])
let call = spyer.findCall((args) => args[0] === assertSecondArgs)
assert.strictEqual(spyer.secondCall, call)
assert.strictEqual(call[0], assertSecondArgs)
call = spyer.findCall((args) => args[0].b === assertThirdArgs[0].b)
assert.strictEqual(spyer.thirdCall, call)
assert.strictEqual(call[0], assertThirdArgs[0])
assert.strictEqual(call[1], assertThirdArgs[1])
call = spyer.findCall((args) => typeof args[0] === 'string')
assert.strictEqual(spyer.firstCall, call)
assert.strictEqual(call[0], assertFirstArgs)
call = spyer.findCall(() => false)
assert.strictEqual(call, null)
})
})
})
@ -324,59 +295,4 @@ t.describe('#stub()', function() {
s.reset()
assert.strictEqual(s(), 'success')
})
t.test('should support chaining', function() {
let s = stub()
s.reset()
.returnWith(function() {})
.resolves(null)
.rejects(null)
.throws(null)
.returns(null)
.reset()
})
t.test('onCall should throw if not a number', function() {
let s = stub()
assert.throws(function() { s.onCall(undefined) })
assert.throws(function() { s.onCall([]) })
assert.throws(function() { s.onCall({}) })
assert.throws(function() { s.onCall('') })
assert.throws(function() { s.onCall('asdf') })
})
t.test('should support arbitrary call returns', function() {
let s = stub()
s.onCall(0).returns(1)
.onCall(1).returns(2)
assert.strictEqual(s(), 1)
assert.strictEqual(s(), 2)
s.reset()
assert.strictEqual(s(), undefined)
assert.strictEqual(s(), undefined)
})
t.test('should send propert arguments in defined calls', function() {
const assertInput = { a : 1 }
const assertReturns = { b: 3 }
let ourCallCount = 0
let s = stub()
s.onCall(3).returnWith(function(input) {
ourCallCount++
assert.strictEqual(input, assertInput)
return assertReturns
})
assert.strictEqual(s(null), undefined)
assert.strictEqual(s(null), undefined)
assert.strictEqual(s(null), undefined)
assert.strictEqual(s(assertInput), assertReturns)
assert.ok(s.called)
assert.strictEqual(s.callCount, 4)
assert.strictEqual(ourCallCount, 1)
assert.strictEqual(s.getCall(3)[0], assertInput)
assert.strictEqual(s(null), undefined)
})
})

View file

@ -1,935 +0,0 @@
import path from 'path'
import fs from 'fs'
import assert from '../lib/assert.mjs'
import t from '../lib/eltro.mjs'
import { Builder, Counter } from './watch/builder.mjs'
import Watcher from '../lib/watch/index.mjs'
import * as is from '../lib/watch/is.mjs'
const builder = new Builder()
let watcher
function htTimeToMs(end) {
return end[0] * 1000 + Math.round(end[1] / 1000000)
}
t.before(function() {
return builder.init()
})
t.afterEach(function(done) {
if (watcher && !watcher.isClosed()) {
watcher.on('close', done)
watcher.close()
} else {
done()
}
})
t.after(function() {
if (builder) {
return builder.cleanup()
}
})
t.describe('watcher', function() {
t.describe('process events', function() {
t.test('should emit `close` event', function(done) {
var file = 'home/a/file1'
var fpath = builder.getPath(file)
watcher = new Watcher(fpath, function() {})
watcher.on('close', done)
watcher.close()
})
t.test('should emit `ready` event when watching a file', function(done) {
var file = 'home/a/file1'
var fpath = builder.getPath(file)
watcher = new Watcher(fpath)
watcher.on('ready', done)
})
t.test('should emit `ready` event when watching a directory recursively', function(done) {
var dir = builder.getPath('home')
watcher = new Watcher(dir, { recursive: true })
watcher.on('ready', done)
})
t.test('should emit `ready` properly in a composed watcher', function(done) {
var dir1 = builder.getPath('home/a')
var dir2 = builder.getPath('home/b')
var file = builder.getPath('home/b/file1')
watcher = new Watcher([dir1, dir2, file], { recursive: true })
watcher.on('ready', done)
})
})
t.describe('watch for files', function() {
t.test('should watch a single file and keep watching', function(done) {
var counter = new Counter(done, 3)
var file = 'home/a/file1'
var fpath = builder.getPath(file)
watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) {
assert.strictEqual(fpath, name)
counter.count()
})
watcher.on('ready', function() {
counter.waitForCount(builder.modify(file))
.then(() => counter.waitForCount(builder.modify(file)))
.then(() => counter.waitForCount(builder.modify(file)))
})
})
t.test('should watch files inside a directory', function(done) {
var fpath = builder.getPath('home/a')
var set = new Set()
set.add(builder.getPath('home/a/file1'))
set.add(builder.getPath('home/a/file2'))
Promise.all([
builder.newFile('home/a/file1'),
builder.newFile('home/a/file2'),
]).then(() => {
watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) {
set.delete(name)
if (!set.size) {
done()
}
})
watcher.on('ready', function() {
Promise.all([
builder.modify('home/a/file1'),
builder.modify('home/a/file2'),
]).then()
})
})
})
t.test('should debounce multiple triggers', function(done) {
var counter = new Counter()
var file = 'home/a/file2'
var fpath = builder.getPath(file)
var start = process.hrtime()
var middle = start
var end = start
watcher = new Watcher(fpath, { delay: 100 }, function(evt, name) {
if (fpath === name) counter.count()
})
watcher.on('ready', function() {
builder.modify(file)
.then(() => builder.modify(file))
.then(() => builder.modify(file))
.then(() => {
middle = htTimeToMs(process.hrtime(start))
return counter.waitForCount()
})
.then(() => {
assert.strictEqual(counter.counter, 1)
end = htTimeToMs(process.hrtime(start))
assert.ok(end - middle > 50)
assert.ok(end >= 100)
done()
})
.catch(done)
})
})
t.test('should listen to new created files', function(done) {
var home = builder.getPath('home')
var counter = new Counter()
var newfile1 = 'home/a/newfile' + builder.randomName()
var newfile2 = 'home/a/newfile' + builder.randomName()
var set = new Set([
builder.getPath(newfile1),
builder.getPath(newfile2),
])
var changes = []
var extra = []
watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) {
if (set.has(name)) {
changes.push(name)
counter.count()
} else {
extra.push(name)
}
})
watcher.on('ready', function() {
counter.waitForCount(builder.newFile(newfile1))
.then(() => counter.waitForCount(builder.newFile(newfile2)))
.then(() => {
assert.deepStrictEqual(changes, [...set.values()])
// assert.deepStrictEqual(extra, [])
done()
})
.catch(done)
})
})
t.test('should error when parent gets deleted before calling fs.watch', function(done) {
var fpath = builder.getPath('home/a/removeme/file1')
builder.newFile('home/a/removeme/file1')
.then(() => {
watcher = new Watcher(fpath, null, null, { fs: { watch: function(path, options) {
builder.removeSync('home/a/removeme')
return fs.watch(path, options)
} } })
watcher.on('error', done.finish(function(err) {
assert.deepStrictEqual(watcher.listeners, [])
}))
})
})
})
t.describe('watch for directories', function() {
t.test('should watch directories inside a directory', function(done) {
var home = builder.getPath('home/c')
var dir = builder.getPath('home/c/removeme')
builder.createDirectory('home/c/removeme').then(() => {
watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) {
if (name === dir && evt === 'remove') {
done()
}
})
watcher.on('ready', function() {
builder.remove('home/c/removeme').catch(done)
})
})
})
t.test('should watch new created directories', function(done) {
var home = builder.getPath('home')
builder.remove('home/new').then(() => {
watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) {
if (name === builder.getPath('home/new/file1')) {
done()
}
})
watcher.on('ready', function() {
builder.newFile('home/new/file1')
.then(() => builder.modify('home/new/file1'))
.catch(done)
})
})
})
t.test('should not watch new created directories which are being skipped', function(done) {
var counter = new Counter(done, 1)
var home = builder.getPath('home')
var options = {
delay: 0,
recursive: true,
skip: function(filePath) {
if (/ignored/.test(filePath)) {
counter.count()
return true
}
return false
}
}
builder.remove('home/ignored/file').then(() => {
watcher = new Watcher(home, options, function(evt, name) {
assert.fail("should not watch new created directories which are being skipped event detect: " + name)
})
watcher.on('ready', function() {
builder.newFile('home/ignored/file')
.catch(done)
})
})
})
t.test('should keep watching after removal of sub directory', function(done) {
var counter = new Counter(done, 3)
var home = builder.getPath('home')
var file1 = builder.getPath('home/e/file1')
var file2 = builder.getPath('home/e/file2')
var dir = builder.getPath('home/e/sub')
var set = new Set()
Promise.all([
builder.newFile('home/e/sub/testfile'),
builder.newFile('home/e/file1'),
builder.newFile('home/e/file2'),
]).then(() => {
watcher = new Watcher(home, { delay: 0, recursive: true }, function(evt, name) {
if (name === dir || name === file1 || name === file2) {
if (!set.has(name)) {
set.add(name)
counter.count()
}
}
})
watcher.on('ready', function() {
builder.remove('home/e/sub')
builder.modify('home/e/file1')
builder.modify('home/e/file2')
})
})
})
t.test('should watch new directories without delay', function(done) {
var counter = new Counter(done, 1)
var home = builder.getPath('home')
builder.remove('home/new').then(() => {
watcher = new Watcher(home, { delay: 200, recursive: true }, function(evt, name) {
if (name === builder.getPath('home/new/file1')) {
counter.count()
}
})
watcher.on('ready', function() {
builder.newFile('home/new/file1')
.then(() => builder.modify('home/new/file1'))
.then(() => builder.modify('home/new/file1'))
})
})
})
t.test('should trigger changed at the end of all debounce', function(done) {
var counter = new Counter()
var fpath = builder.getPath('home/a/')
watcher = new Watcher(fpath, { delay: 100 })
watcher.on('change', function(evt, name) {
counter.count()
})
watcher.on('changed', done.finish(function(evt, name) {
assert.strictEqual(counter.counter, 3)
}))
watcher.on('ready', done.wrap(function() {
builder.modify('home/a/file1')
.then(() => builder.modify('home/a/file2'))
.then(() => builder.modify('home/a/file3'))
.catch(done)
}))
})
t.test('should trigger changed for each change when delay is zero', function(done) {
var counter = new Counter(done, 3)
var set = new Set([
builder.getPath('home/a/file1'),
builder.getPath('home/a/file2'),
builder.getPath('home/a/file3'),
])
var fpath = builder.getPath('home/a')
watcher = new Watcher(fpath, { delay: 0 })
watcher.on('changed', done.finish(function(evt, name) {
if (set.has(name)) {
set.delete(name)
counter.count()
}
}))
watcher.on('ready', function() {
builder.modify('home/a/file1')
.then(() => builder.modify('home/a/file2'))
.then(() => builder.modify('home/a/file3'))
.catch(done)
})
})
t.test('should error when directory gets deleted before calling fs.watch', function(done) {
var dir = 'home/c/removeme'
var fpath = builder.getPath(dir)
builder.createDirectory(dir).then(() => {
watcher = new Watcher(fpath, null, null, { fs: { watch: function(path, options) {
builder.removeSync(dir)
return fs.watch(path, options)
} } })
watcher.on('error', done.finish(function(err) {
assert.deepStrictEqual(watcher.listeners, [])
}))
})
})
})
t.describe('file events', function() {
var file = 'home/a/file1'
var fpath = builder.getPath(file)
t.beforeEach(function() {
return builder.newFile(file)
})
t.test('should identify `remove` event', function(done) {
watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) {
if (evt === 'remove' && name === fpath) {
done()
}
})
watcher.on('ready', function() {
builder.remove(file).catch(done)
})
})
t.test('should identify `remove` event on directory', function(done) {
var dir = 'home/a/removeme'
var home = builder.getPath('home/a')
var fpath = builder.getPath(dir)
builder.createDirectory('home/a/removeme').then(done.wrap(function() {
watcher = new Watcher(home, { delay: 0 }, function(evt, name) {
if (evt === 'remove' && name === fpath) done()
})
watcher.on('ready', function() {
builder.remove(dir).catch(done)
})
}))
})
t.test('should be able to handle many events on deleting', function(done) {
var dir = 'home/a'
var fpath = builder.getPath(dir)
builder.newRandomFiles(dir, 100).then(names => {
var counter = new Counter(done, names.length)
watcher = new Watcher(fpath, { delay: 10 }, function(evt, name) {
if (evt === 'remove') counter.count()
})
watcher.on('ready', function() {
Promise.all(names.map(x => builder.remove(path.join(dir, x)))).catch(done)
})
})
})
t.test('should identify `update` event', function(done) {
var file = 'home/b/file1'
var fpath = builder.getPath(file)
builder.newFile(file).then(() => {
watcher = new Watcher(fpath, { delay: 0 }, function(evt, name) {
if (evt === 'update' && name === fpath) done()
})
watcher.on('ready', function() {
builder.modify(file).catch(done)
})
})
})
t.test('should report `update` on new files', function(done) {
var dir = builder.getPath('home/a')
var file = 'home/a/newfile_' + builder.randomName()
var fpath = builder.getPath(file)
watcher = new Watcher(dir, { delay: 0 }, function(evt, name) {
if (evt === 'update' && name === fpath) done()
})
watcher.on('ready', function() {
builder.newFile(file).catch(done)
})
})
})
t.describe('options', function() {
t.describe('recursive', function() {
t.test('should watch recursively with `recursive: true` option', function(done) {
var dir = builder.getPath('home')
var file = builder.getPath('home/bb/file1')
watcher = new Watcher(dir, { delay: 0, recursive: true }, function(evt, name) {
if (file === name) {
done()
}
})
watcher.on('ready', function() {
builder.modify('home/bb/file1').catch(done)
})
})
})
t.test('should store original paths in object', function() {
var dir = builder.getPath('home')
watcher = new Watcher(dir)
assert.deepStrictEqual(watcher.originalPaths, [dir])
})
t.test('should store all original paths in object', function() {
var dir1 = builder.getPath('home/a')
var dir2 = builder.getPath('home/b')
watcher = new Watcher([dir1, dir2])
assert.deepStrictEqual(watcher.originalPaths, [dir1, dir2])
})
t.describe('encoding', function() {
let options = {
delay: 0,
encoding: 'unknown'
};
var fdir = builder.getPath('home/a')
var file = 'home/a/file1'
var fpath = builder.getPath(file)
t.before(() => {
return builder.newFile(file)
})
t.test('should throw on invalid encoding', function(done) {
options.encoding = 'unknown'
try {
watcher = new Watcher(fdir, options)
} catch (e) {
done()
}
})
t.test('should accept an encoding string', function(done) {
options.encoding = 'utf8'
watcher = new Watcher(fdir, options, done.finish(function(evt, name) {
assert.strictEqual(name.toString(), fpath)
}))
watcher.on('ready', function() {
builder.modify(file).catch(done)
})
})
t.test('should support buffer encoding', function(done) {
options.encoding = 'buffer'
watcher = new Watcher(fdir, options, done.finish(function(evt, name) {
assert.ok(Buffer.isBuffer(name), 'not a Buffer')
assert.strictEqual(name.toString(), fpath)
}))
watcher.on('ready', function() {
builder.modify(file).catch(done)
})
})
t.test('should support base64 encoding', function(done) {
options.encoding = 'base64'
watcher = new Watcher(fdir, options, done.finish(function(evt, name) {
assert.strictEqual(
name,
Buffer.from(fpath).toString('base64'),
'wrong base64 encoding'
)
}))
watcher.on('ready', function() {
builder.modify(file).catch(done)
})
})
t.test('should support hex encoding', function(done) {
options.encoding = 'hex'
watcher = new Watcher(fdir, options, done.finish(function(evt, name) {
assert.strictEqual(
name,
Buffer.from(fpath).toString('hex'),
'wrong hex encoding'
)
}))
watcher.on('ready', function() {
builder.modify(file).catch(done)
})
})
})
t.describe('skip', function() {
t.test('should only watch non-skipped directories', function(done) {
var matchRegularDir = false
var matchIgnoredDir = false
var counter = new Counter(done.finish(function() {
assert(matchRegularDir, 'watch failed to detect regular file')
assert(!matchIgnoredDir, 'fail to ignore path `deep_node_modules`')
}), 1, true)
var options = {
delay: 0,
recursive: true,
skip: function(name) {
return /deep_node_modules/.test(name)
}
}
watcher = new Watcher(builder.getPath('home'), options, function(evt, name) {
if (/deep_node_modules/.test(name)) {
matchIgnoredDir = true
} else {
matchRegularDir = true
}
counter.count()
})
watcher.on('ready', function() {
counter.startCounting()
builder.modify('home/deep_node_modules/ma/file1')
.then(() => builder.modify('home/b/file1'))
.catch(done)
})
})
t.test('should only report non-skipped files', function(done) {
var dir = builder.getPath('home')
var file1 = 'home/bb/file1'
var file2 = 'home/bb/file2'
var counter = new Counter(done.finish(function() {
assert.strictEqual(matchIgnoredFile, false, 'home/bb/file1 should be ignored')
}), 1, true)
var options = {
delay: 0,
recursive: true,
skip: function(name) {
return /file1/.test(name)
}
}
var matchIgnoredFile = false
watcher = new Watcher(dir, options, function(evt, name) {
if (name === builder.getPath(file1)) {
matchIgnoredFile = true
}
counter.count()
})
watcher.on('ready', function() {
counter.startCounting()
builder.modify(file2)
.then(() => builder.modify(file1))
.catch(done)
})
})
t.test('should be able to skip directly with regexp', function(done) {
var dir = builder.getPath('home')
var file1 = 'home/bb/file1'
var file2 = 'home/bb/file2'
var counter = new Counter(done.finish(function() {
assert.strictEqual(matchIgnoredFile, false, 'home/bb/file1 should be ignored')
}), 1, true)
var options = {
delay: 0,
recursive: true,
skip: /file1/
}
var times = 0
var matchIgnoredFile = false
watcher = new Watcher(dir, options, function(evt, name) {
if (name === builder.getPath(file1)) {
matchIgnoredFile = true
}
counter.count()
})
watcher.on('ready', function() {
counter.startCounting()
builder.modify(file2)
.then(() => builder.modify(file1))
.catch(done)
})
})
t.test('should be able to skip subdirectories with `skip` flag', function(done) {
var home = builder.getPath('home')
var options = {
delay: 0,
recursive: true,
manualRecursive: true,
skip: function(name) {
if (/\/deep_node_modules/.test(name)) return true
return false
}
}
watcher = new Watcher(home, options)
watcher.on('ready', done.finish(function() {
let homeFiltered = builder.getAllDirectories().filter(function(name) {
return !/\/deep_node_modules/.test(name)
}).sort()
let watchersPaths = watcher.listeners.map(x => x.path).sort()
assert.deepStrictEqual(watchersPaths, homeFiltered)
}))
})
})
t.describe('filter', function() {
t.test('should not have impact on watched directories', function(done) {
var matchNonFilterDir = false
var matchFilteredDir = false
var counter = new Counter(done.finish(function() {
assert(!matchNonFilterDir, 'watch should not detect non-filter file')
assert(matchFilteredDir, 'watch failed to detect filter path `deep_node_modules`')
}), 1, true)
var options = {
delay: 0,
recursive: true,
filter: function(name) {
return /deep_node_modules/.test(name)
}
}
watcher = new Watcher(builder.getPath('home'), options, function(evt, name) {
if (/deep_node_modules/.test(name)) {
matchFilteredDir = true
} else {
matchNonFilterDir = true
}
counter.count()
})
watcher.on('ready', function() {
counter.startCounting()
builder.modify('home/b/file1')
.then(() => builder.modify('home/deep_node_modules/ma/file1'))
.catch(done)
})
})
t.test('should only report filtered files', function(done) {
var dir = builder.getPath('home')
var file1 = 'home/bb/file1'
var file2 = 'home/bb/file2'
var matchFilterFile = false
var counter = new Counter(done.finish(function() {
assert.strictEqual(matchFilterFile, true, 'home/bb/file1 should be visible')
}), 1, true)
var options = {
delay: 0,
recursive: true,
filter: function(name) {
return /file1/.test(name)
}
}
watcher = new Watcher(dir, options, function(evt, name) {
if (name === builder.getPath(file1)) {
matchFilterFile = true
}
counter.count()
})
watcher.on('ready', function() {
counter.startCounting()
builder.modify(file2)
.then(() => builder.modify(file1))
.catch(done)
})
})
t.test('should be able to filter directly with regexp', function(done) {
var dir = builder.getPath('home')
var file1 = 'home/bb/file1'
var file2 = 'home/bb/file2'
var counter = new Counter(done.finish(function() {
assert.strictEqual(matchFilterFile, true, 'home/bb/file1 should be visible')
}), 1, true)
var options = {
delay: 0,
recursive: true,
filter: /file1/
}
var matchFilterFile = false
watcher = new Watcher(dir, options, function(evt, name) {
if (name === builder.getPath(file1)) {
matchFilterFile = true
}
counter.count()
})
watcher.on('ready', function() {
counter.startCounting()
builder.modify(file2)
.then(() => builder.modify(file1))
.catch(done)
})
})
t.test('should not filter subdirectories with `filter` flag', function(done) {
var home = builder.getPath('home')
var options = {
delay: 0,
recursive: true,
manualRecursive: true,
filter: function(name) {
if (/\/deep_node_modules/.test(name)) return true
return false
}
}
watcher = new Watcher(home, options)
watcher.on('ready', done.finish(function() {
let homeFiltered = builder.getAllDirectories().sort()
let watchersPaths = watcher.listeners.map(x => x.path).sort()
assert.deepStrictEqual(watchersPaths, homeFiltered)
}))
})
})
})
t.describe('parameters', function() {
t.test('should throw error on non-existed file', function(done) {
var somedir = builder.getPath('home/somedir')
watcher = new Watcher(somedir)
watcher.on('error', done.finish(function(err) {
assert.match(err.message, /not exist/)
}))
})
t.test('should accept filename as Buffer', function(done) {
var fpath = builder.getPath('home/a/file1')
watcher = new Watcher(Buffer.from(fpath), { delay: 0 }, done.finish(function(evt, name) {
assert.strictEqual(name, fpath)
}))
watcher.on('ready', function() {
builder.modify('home/a/file1').catch(done)
})
})
t.test('should compose array of files or directories', function(done) {
var counter = new Counter(done, 2)
var file1 = 'home/a/file1'
var file2 = 'home/a/file2'
var fpaths = [
builder.getPath(file1),
builder.getPath(file2)
]
var set = new Set(fpaths)
Promise.all([
builder.newFile(file1),
builder.newFile(file2),
]).then(function() {
watcher = new Watcher(fpaths, { delay: 0 }, function(evt, name) {
if (set.has(name)) {
set.delete(name)
counter.count()
}
})
watcher.on('ready', function() {
Promise.all([
builder.modify(file1),
builder.modify(file2),
]).catch(done)
})
})
})
t.test('should filter duplicate events for composed watcher', function(done) {
var counter = new Counter(done, 2)
var home = 'home'
var dir = 'home/a'
var file1 = 'home/a/file1'
var file2 = 'home/a/file2'
var fpaths = [
builder.getPath(home),
builder.getPath(dir),
builder.getPath(file1),
builder.getPath(file2)
]
var changes = []
watcher = new Watcher(fpaths, { delay: 100, recursive: true }, function(evt, name) {
changes.push(name)
counter.count()
})
watcher.on('ready', function() {
new Promise(res => {
counter.updateRes(res)
builder.modify(file1)
.then(() => builder.modify(file2))
})
.then(() => builder.delay(50))
.then(() => {
assert.deepStrictEqual(
changes,
[fpaths[2], fpaths[3]]
)
done()
}).catch(done)
})
})
})
t.describe('watcher object', function() {
t.test('should using watcher object to watch', function(done) {
var dir = builder.getPath('home/a')
var file = 'home/a/file1'
var fpath = builder.getPath(file)
watcher = new Watcher(dir, { delay: 0 })
watcher.on('change', done.finish(function(evt, name) {
assert.strictEqual(evt, 'update')
assert.strictEqual(name, fpath)
}))
watcher.on('ready', done.wrap(function() {
builder.modify(file).catch(done)
}))
})
t.describe('close()', function() {
t.test('should close a watcher using .close()', function(done) {
var dir = builder.getPath('home/a')
var file = 'home/a/file1'
var times = 0
watcher = new Watcher(dir, { delay: 0 })
watcher.on('change', function(evt, name) {
times++
})
watcher.on('ready', function() {
watcher.close()
builder.modify(file)
.then(() => builder.delay(50))
.then(() => builder.modify(file))
.then(() => builder.delay(50))
.then(() => {
assert(watcher.isClosed(), 'watcher should be closed')
assert.strictEqual(times, 0, 'failed to close the watcher')
done()
}).catch(done)
})
})
t.test('should not watch after .close() is called', function(done) {
var dir = builder.getPath('home')
watcher = new Watcher(dir, { delay: 0, recursive: true })
watcher.on('ready', function() {
watcher.close()
})
watcher.on('close', done.finish(function(dirs) {
assert.strictEqual(watcher.listeners.length, 0)
}))
})
})
})
})

View file

@ -1,248 +0,0 @@
import fs from 'fs/promises'
import fsSync from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let structure = `
home/
a/
file1
file2
b/
file1
file2
c/
bb/
file1
file2
d/
file1
file2
e/
file1
file2
sub/
deep_node_modules/
ma/
file1
file2
mb/
file1
file2
mc/
`
let code = structure
.split('\n')
.filter(line => line)
.map(function(line) {
return {
indent: line.length - line.replace(/^\s+/,'').length,
type: /\/$/.test(line) ? 'dir': 'file',
text: line.replace(/^\s+|\s*\/\s*|\s+$/g, '')
}
})
function join(arr) {
return arr.join('/')
}
function transform(arr) {
let result = []
let temp = []
let indent = 0
arr.forEach(function(line) {
if (!line.text) {
return
}
else if (!line.indent) {
temp.push(line.text)
result.push({type: line.type, text: join(temp) })
}
else if (indent < line.indent) {
temp.push(line.text)
result[result.length - 1].type = 'dir'
result.push({type: line.type, text: join(temp) })
}
else if (indent === line.indent) {
temp.pop()
temp.push(line.text)
result.push({type: line.type, text: join(temp) })
}
else if(indent > line.indent) {
temp.pop()
temp.pop()
temp.push(line.text)
result.push({type: line.type, text: join(temp) })
}
indent = line.indent
})
return result
}
function gracefully(promise) {
return promise.catch(function(err) {
console.log(err)
})
}
function emptyFile(target) {
let folder = target.slice(0, target.lastIndexOf('/'))
/*return fs.mkdir(folder, { recursive: true, force: true })
.then(() => fs.open(target, 'w').then(fd => fd.close()))*/
return fs.stat(folder)
.catch(() => fs.mkdir(folder, { recursive: true, force: true }))
.then(() => {
return fs.open(target, 'w').then(fd => fd.close())
})
}
export function Builder() {
this.root = path.join(__dirname, '__TREE__')
this.transformed = transform(code)
}
Builder.prototype.init = async function() {
await Promise.all(
this.transformed.filter(line => line.type === 'dir').map(line => {
let target = path.join(this.root, line.text)
return gracefully(fs.mkdir(target, { recursive: true }))
})
)
await Promise.all(
this.transformed.filter(line => line.type !== 'dir').map(line => {
let target = path.join(this.root, line.text)
return gracefully(emptyFile(target))
})
)
}
Builder.prototype.getPath = function(fpath, sub) {
return path.join(this.root, fpath, sub || '')
}
Builder.prototype.modify = function(fpath, delay) {
if (delay) throw new Error('remove delay')
let filePath = this.getPath(fpath)
return fs.appendFile(filePath, 'hello')
}
Builder.prototype.remove = function(fpath) {
let filePath = this.getPath(fpath)
return fs.rm(filePath, { recursive: true, force: true })
.catch(() =>
this.delay(100)
.then(() => fs.rm(filePath, { recursive: true, force: true }))
)
}
Builder.prototype.removeSync = function(fpath) {
let filePath = this.getPath(fpath)
return fsSync.rmSync(filePath, { recursive: true, force: true })
}
Builder.prototype.newFile = function(fpath, delay) {
if (delay) throw new Error('remove delay')
let filePath = this.getPath(fpath)
return emptyFile(filePath)
}
Builder.prototype.createDirectory = function(fpath) {
let filePath = this.getPath(fpath)
return fs.mkdir(filePath, { recursive: true })
}
Builder.prototype.randomName = function() {
return Math.random().toString(36).substring(2, 14)
}
Builder.prototype.newRandomFiles = function(fpath, count) {
return Promise.all(
new Array(count).fill(0).map(() => {
let name = this.randomName()
let filePath = this.getPath(fpath, name)
return emptyFile(filePath).then(() => name)
})
)
}
Builder.prototype.cleanup = function() {
return fs.rm(this.root, { recursive: true, force: true })
}
Builder.prototype.getAllDirectories = function() {
function walk(dir) {
let ret = []
fsSync.readdirSync(dir).forEach(function(d) {
let fpath = path.join(dir, d)
if (fsSync.statSync(fpath).isDirectory()) {
ret.push(fpath)
ret = ret.concat(walk(fpath))
}
})
return ret
}
return walk(this.root)
}
Builder.prototype.delay = function(delay) {
return new Promise(res => {
setTimeout(res, delay)
})
}
export function Counter(res, countTotal, waitForSignal = false) {
this._res = res
this.counter = 0
this._countTotal = countTotal
this._startCount = !waitForSignal
this._gotSignal = false
this._signal = null
}
Counter.prototype.waitForCount = function(promise) {
if (!promise) {
promise = Promise.resolve()
}
return new Promise(res => {
promise.then(() => {
this._signal = res
this.hasSignal()
})
})
}
Counter.prototype.updateRes = function(res) {
this._res = res
}
Counter.prototype.hasSignal = function() {
if (this._gotSignal && this._signal) {
this._gotSignal = false
let temp = this._signal
this._signal = null
temp()
}
}
Counter.prototype.startCounting = function() {
this._startCount = true
}
Counter.prototype.count = function() {
if (!this._startCount) return
this._gotSignal = true
this.counter++
if (this.counter === this._countTotal && this._res) {
this._res()
}
this.hasSignal()
}