Skip to content
This repository has been archived by the owner on May 22, 2024. It is now read-only.

Commit

Permalink
fix: initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx committed Jan 21, 2018
1 parent c7998a8 commit 8e0790b
Show file tree
Hide file tree
Showing 15 changed files with 367 additions and 212 deletions.
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@
"version": "0.0.1",
"author": "Jeff Dickey @jdxcode",
"bugs": "https://github.com/jdxcode/fancy-mocha/issues",
"dependencies": {},
"dependencies": {
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"lodash": "^4.17.4",
"stdout-stderr": "^0.1.4"
},
"devDependencies": {
"@dxcli/dev-semantic-release": "^0.1.0",
"@dxcli/dev-test": "^0.7.0",
"@dxcli/dev-tslint": "^0.0.15",
"@types/chai": "^4.1.1",
"@types/chai-as-promised": "^7.1.0",
"@types/lodash": "^4.14.93",
"@types/mocha": "^2.2.46",
"@types/node": "^9.3.0",
"eslint": "^4.16.0",
"eslint-config-dxcli": "^1.1.4",
Expand Down
5 changes: 5 additions & 0 deletions src/chai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as chai from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised)

export const {expect} = chai
10 changes: 10 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const original = process.env

export default (env: {[k: string]: string}) => ({
before() {
process.env = {...env}
},
after() {
process.env = original
}
})
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export const add = (a: number, b: number) => a + b
export * from './chai'

import test from './test'
export {test}
export default test
17 changes: 17 additions & 0 deletions src/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as _ from 'lodash'

/**
* mocks an object's property
*/
export default (object: any, path: string, value: any) => {
let original = _.get(object, path)
return {
before() {
original = _.get(object, path)
_.set(object, path, value)
},
finally() {
_.set(object, path, original)
}
}
}
19 changes: 19 additions & 0 deletions src/stdmock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as mock from 'stdout-stderr'

const create = <T extends 'stdout' | 'stderr'>(std: T) => () => {
const _finally = () => mock[std].stop()
return {
before() {
mock[std].start()
return {
get [std]() { return mock[std].output }
} as {[P in T]: string}
},
after: _finally,
catch: _finally,
finally: _finally,
}
}

export const stdout = create('stdout')
export const stderr = create('stderr')
210 changes: 210 additions & 0 deletions src/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import * as mocha from 'mocha'

import env from './env'
import mock from './mock'
import {stderr, stdout} from './stdmock'

export type Extension<O extends object, A1, A2, A3, A4> = (a1: A1, a2: A2, a3: A3, a4: A4) => Filters<O>
export interface Filters<C extends object> {
before?(input: any): Promise<C> | C | void
after?(context: C): Promise<any> | any
catch?(context: C, err: Error): Promise<any> | any
finally?(context: C): Promise<any> | any
}

export type MochaDone = (error?: any) => any

// export type TestCallback<T, C, TR> = (this: T, context: C & {done: MochaDone}) => TR
export type TestCallback<T, C, TR> = (this: T, context: C) => TR
export type ItCallback<C = any> = TestCallback<mocha.ITestCallbackContext, C, Promise<any> | any>
export type DescribeCallback = TestCallback <mocha.ISuiteCallbackContext, {}, void>

export type Extensions = [{[k: string]: object}, {[k: string]: [object, any]}]

export interface It<C> {
(expectation: string, cb: ItCallback<C>): mocha.ITest
only(expectation: string, cb: ItCallback<C>): mocha.ITest
skip(expectation: string, cb: ItCallback<C>): void
}

export interface Describe {
(expectation: string, cb: DescribeCallback): mocha.ISuite
only(expectation: string, cb: DescribeCallback): mocha.ISuite
skip(expectation: string, cb: (this: mocha.ISuiteCallbackContext) => void): void
}

export interface TestBase<E extends Extensions, C = {}> {
/**
* wrapper for mocha's describe()
*/
describe: Describe
/**
* wrapper for mocha's it()
*/
it: It<C>
/**
* currently loaded filters
*/
filters: Filters<any>[]
/**
* add your own filters
*
* @example
* import test from 'fancy-mocha'
*
* // this will run before each command and add {foo: 100} to the context object in the callback
* test.extend(myopts => {
* return {
* before(): {
* console.log('do something with myopts')
* return {foo: 100}
* }
* }
* })
*/
extend<K extends string, O extends object>(key: K, filter: Extension<O, void, void, void, void>): Test<AddExtension0<E, K, O>, C>
extend<K extends string, O extends object, A1>(key: K, filter: Extension<O, A1, void, void, void>): Test<AddExtension1<E, K, O, A1>, C>
extend<K extends string, O extends object, A1, A2>(key: K, filter: Extension<O, A1, A2, void, void>): Test<AddExtension2<E, K, O, A1, A2>, C>
extend<K extends string, O extends object, A1, A2, A3>(key: K, filter: Extension<O, A1, A2, A3, void>): Test<AddExtension3<E, K, O, A1, A2, A3>, C>
extend<K extends string, O extends object, A1, A2, A3, A4>(key: K, filter: Extension<O, A1, A2, A3, A4>): Test<AddExtension4<E, K, O, A1, A2, A3, A4>, C>
}
export type Test<E extends Extensions, C> = TestBase<E, C> &
{[P in keyof E[0]]: FilterFn0<E, C, E[0][P]>} &
{[P in keyof E[1]]: FilterFn1<E, C, E[1][P][0], E[1][P][1]>} &
{[P in keyof E[1]]: FilterFn2<E, C, E[1][P][0], E[1][P][1], E[1][P][2]>} &
{[P in keyof E[1]]: FilterFn3<E, C, E[1][P][0], E[1][P][1], E[1][P][2], E[1][P][3]>} &
{[P in keyof E[1]]: FilterFn4<E, C, E[1][P][0], E[1][P][1], E[1][P][2], E[1][P][3], E[1][P][4]>}

export type AddExtension0<E extends Extensions, K extends string, O> = [E[0] & {[P in K]: O}, E[1]]
export type AddExtension1<E extends Extensions, K extends string, O, A1> = [E[0], E[1] & {[P in K]: [O, A1]}]
export type AddExtension2<E extends Extensions, K extends string, O, A1, A2> = [E[0], E[1] & {[P in K]: [O, A1, A2]}]
export type AddExtension3<E extends Extensions, K extends string, O, A1, A2, A3> = [E[0], E[1] & {[P in K]: [O, A1, A2, A3]}]
export type AddExtension4<E extends Extensions, K extends string, O, A1, A2, A3, A4> = [E[0], E[1] & {[P in K]: [O, A1, A2, A3, A4]}]
export type FilterFn0<E extends Extensions, C, O extends object> = () => Test<E, C & O>
export type FilterFn1<E extends Extensions, C, O extends object, A1> = (a1?: A1) => Test<E, C & O>
export type FilterFn2<E extends Extensions, C, O extends object, A1, A2> = (a1?: A1, a2?: A2) => Test<E, C & O>
export type FilterFn3<E extends Extensions, C, O extends object, A1, A2, A3> = (a1?: A1, a2?: A2, a3?: A3) => Test<E, C & O>
export type FilterFn4<E extends Extensions, C, O extends object, A1, A2, A3, A4> = (a1?: A1, a2?: A2, a3?: A3, a4?: A4) => Test<E, C & O>

// This is an assign function that copies full descriptors
function completeAssign(target: any, ...sources: any[]) {
if (!target) target = sources.find(f => !!f)
sources.forEach(source => {
if (!source) return
let descriptors: any = Object.keys(source).reduce((descriptors: any, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key)
return descriptors
}, {})
// by default, Object.assign copies enumerable Symbols too
Object.getOwnPropertySymbols(source).forEach(sym => {
let descriptor: any = Object.getOwnPropertyDescriptor(source, sym)
if (descriptor.enumerable) {
descriptors[sym] = descriptor
}
})
Object.defineProperties(target, descriptors)
})
return target
}

const test = <F extends Extensions = [{}, {}]>(previous?: Test<any, any>): TestBase<F> => {
const filters: Filters<any>[] = previous ? previous.filters : []
const before = async () => {
let context = {}
for (let f of filters) {
if (f.before) {
context = completeAssign(context, await f.before(context))
}
}
return context
}
const _catch = async (err: Error, context?: object) => {
for (let f of filters) {
if (f.catch) await f.catch(context, err)
}
}
const _finally = async (context?: object) => {
for (let f of filters) {
if (f.finally) await f.finally(context)
}
}
const after = async (context?: object) => {
for (let f of filters) {
if (f.after) await f.after(context)
}
}

const __it = (cb?: ItCallback) => async function (this: mocha.ITestCallbackContext) {
let error
try {
const context = await before()
if (cb) await cb.call(this, context)
await after(context)
} catch (err) {
error = err
await _catch(err)
} finally {
await _finally()
}
if (error) throw error
}
const _it = Object.assign((expectation: string, cb?: ItCallback): Mocha.ITest => {
return it(expectation, __it(cb))
}, {
only(expectation: string, cb?: ItCallback): Mocha.ITest {
return it.only(expectation, __it(cb))
},
skip(expectation: string, cb?: ItCallback): void {
return it.skip(expectation, cb)
},
})

const __describe = (cb?: ItCallback) => async function (this: mocha.ITestCallbackContext) {
let error
beforeEach(before)
afterEach(after)
try {
if (cb) await cb.call(this)
} catch (err) {
error = err
await _catch(err)
} finally {
await _finally()
}
if (error) throw error
}
const _describe = Object.assign((expectation: string, cb: DescribeCallback): Mocha.ISuite => {
return describe(expectation, __describe(cb))
}, {
only(expectation: string, cb: DescribeCallback): Mocha.ISuite {
return describe.only(expectation, __describe(cb))
},
skip(expectation: string, cb: (this: mocha.ISuiteCallbackContext) => void): void {
return describe.skip(expectation, cb)
},
})

return {
...previous,
it: _it,
describe: _describe,
filters,
extend(this: any, key: string, extension: Extension<object, any, any, any, any>) {
return test({
...this,
[key](...opts: any[]) {
return test({
...this,
filters: this.filters.concat([(extension as any)(...opts)]),
} as any)
}
})
},
} as any
}

export default test()
.extend('env', env)
.extend('mock', mock)
.extend('stdout', stdout)
.extend('stderr', stderr)
13 changes: 13 additions & 0 deletions test/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// tslint:disable no-console

import {expect, test} from '../src'

describe('stdout', () => {
test
.env({foo: 'BARBAZ'})
.stdout()
.it('logs', output => {
console.log(process.env.foo)
expect(output.stdout).to.equal('BARBAZ\n')
})
})
1 change: 0 additions & 1 deletion test/helpers/init.js

This file was deleted.

9 changes: 0 additions & 9 deletions test/integration/index.test.ts

This file was deleted.

1 change: 0 additions & 1 deletion test/mocha.opts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
--require test/helpers/init.js
--require ts-node/register
--require source-map-support/register
--watch-extensions ts
Expand Down
24 changes: 24 additions & 0 deletions test/mock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// tslint:disable no-console

import * as os from 'os'

import {expect, test} from '../src'

const platform = os.platform()

describe('stdout', () => {
test
.mock(os, 'platform', () => 'foobar')
.stdout()
.it('sets os', output => {
console.log(os.platform())
expect(output.stdout).to.equal('foobar\n')
})

test
.stdout()
.it('resets os', output => {
console.log(os.platform())
expect(output.stdout).to.equal(`${platform}\n`)
})
})
41 changes: 41 additions & 0 deletions test/stdmock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// tslint:disable no-console

import {expect, test} from '../src'

describe('stdout', () => {
test
.stderr()
.stdout()
.it('logs', output => {
console.error('about to write')
console.log('foo')
console.error('written')
expect(output.stdout).to.equal('foo\n')
})

test
.stdout()
.it('logs twice', output => {
console.log('foo')
expect(output.stdout).to.equal('foo\n')
console.log('bar')
expect(output.stdout).to.equal('foo\nbar\n')
})

test
.stderr()
.it('writes to stderr', output => {
console.error('foo')
expect(output.stderr).to.equal('foo\n')
})

test
.stdout()
.stderr()
.it('writes to both', output => {
console.error('foo')
console.log('bar')
expect(output.stderr).to.equal('foo\n')
expect(output.stdout).to.equal('bar\n')
})
})
Loading

0 comments on commit 8e0790b

Please sign in to comment.