From 5116df46a020ae498eec6783cfe9147fc9add015 Mon Sep 17 00:00:00 2001 From: Jack Franklin Date: Wed, 15 Jul 2020 15:31:43 +0100 Subject: Add generic types and update tests (#107) * Add generic types and update tests * Add generic types to `on`, `off` and `emit` to enable some nicer TS usage if you specify what type you're expecting from the `EventData`. * Move the tests to be TypeScript source. This will help catch errors in the tests if there are any type errors. * Create a new test to test the generic types and make sure they pass and error when expected. * Upgrade to Mocha 8. * Did some tidying up of the package.json scripts. * Tweak TS setup to validate tests * Fix d.ts generation and tests Co-authored-by: Jason Miller --- .editorconfig | 1 + .gitignore | 1 + README.md | 2 +- package.json | 35 +++++--- src/index.ts | 14 ++-- test/index.js | 182 ----------------------------------------- test/index_test.ts | 182 +++++++++++++++++++++++++++++++++++++++++ test/test-types-compilation.ts | 43 ++++++++++ test/types.ts | 20 ----- tsconfig.json | 8 +- 10 files changed, 265 insertions(+), 223 deletions(-) delete mode 100644 test/index.js create mode 100644 test/index_test.ts create mode 100644 test/test-types-compilation.ts delete mode 100644 test/types.ts diff --git a/.editorconfig b/.editorconfig index ac0adb7..04d7ef9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,7 @@ insert_final_newline = true [{package.json,.*rc,*.yml}] indent_style = space indent_size = 2 +insert_final_newline = false [*.md] trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index ecdffab..eb570d3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /test-reports /node_modules /npm-debug.log +/index.d.ts package-lock.json .DS_Store .idea diff --git a/README.md b/README.md index 0796e5f..b6a8022 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ const emitter: mitt.Emitter = mitt(); Mitt: Tiny (~200b) functional event emitter / pubsub. -Returns **Mitt** +Returns **Mitt** ### on diff --git a/package.json b/package.json index 90b7b74..a2c2b58 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,13 @@ "esmodules": "dist/mitt.modern.js", "main": "dist/mitt.js", "umd:main": "dist/mitt.umd.js", - "typings": "dist/index.d.ts", + "typings": "index.d.ts", "scripts": { - "test": "npm-run-all --silent typecheck lint testonly", - "testonly": "mocha --require esm test/**/*.js", + "test": "npm-run-all --silent typecheck lint mocha test-types", + "mocha": "mocha test", + "test-types": "tsc test/test-types-compilation.ts --noEmit", "lint": "eslint src test --ext ts --ext js", - "typecheck": "tsc **/*.ts --noEmit", + "typecheck": "tsc --noEmit", "bundle": "microbundle", "build": "npm-run-all --silent clean -p bundle -s docs", "clean": "rimraf dist", @@ -34,8 +35,21 @@ "license": "MIT", "files": [ "src", - "dist" + "dist", + "index.d.ts" ], + "mocha": { + "extension": [ + "ts" + ], + "require": [ + "ts-node/register", + "esm" + ], + "spec": [ + "test/*_test.ts" + ] + }, "eslintConfig": { "extends": [ "developit", @@ -68,7 +82,8 @@ } }, "eslintIgnore": [ - "dist" + "dist", + "index.d.ts" ], "devDependencies": { "@types/chai": "^4.2.11", @@ -82,13 +97,13 @@ "eslint": "^7.1.0", "eslint-config-developit": "^1.2.0", "esm": "^3.2.25", - "microbundle": "^0.12.0", - "mocha": "^7.2.0", + "microbundle": "^0.12.3", + "mocha": "^8.0.1", "npm-run-all": "^4.1.5", "rimraf": "^3.0.2", "sinon": "^9.0.2", "sinon-chai": "^3.5.0", - "ts-node": "^8.10.1", + "ts-node": "^8.10.2", "typescript": "^3.9.3" } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index e640292..c681fd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,8 @@ export type EventType = string | symbol; // An event handler can take an optional event argument // and should not return a value -export type Handler = (event?: any) => void; -export type WildcardHandler = (type: EventType, event?: any) => void +export type Handler = (event?: T) => void; +export type WildcardHandler = (type: EventType, event?: any) => void; // An array of all currently registered event handlers for a type export type EventHandlerList = Array; @@ -13,10 +13,10 @@ export type WildCardEventHandlerList = Array; export type EventHandlerMap = Map; export interface Emitter { - on(type: EventType, handler: Handler): void; + on(type: EventType, handler: Handler): void; on(type: '*', handler: WildcardHandler): void; - off(type: EventType, handler: Handler): void; + off(type: EventType, handler: Handler): void; off(type: '*', handler: WildcardHandler): void; emit(type: EventType, event?: T): void; @@ -38,7 +38,7 @@ export default function mitt(all?: EventHandlerMap): Emitter { * @param {Function} handler Function to call in response to given event * @memberOf mitt */ - on(type: EventType, handler: Handler) { + on(type: EventType, handler: Handler) { const handlers = all.get(type); const added = handlers && handlers.push(handler); if (!added) { @@ -53,7 +53,7 @@ export default function mitt(all?: EventHandlerMap): Emitter { * @param {Function} handler Handler function to remove * @memberOf mitt */ - off(type: EventType, handler: Handler) { + off(type: EventType, handler: Handler) { const handlers = all.get(type); if (handlers) { handlers.splice(handlers.indexOf(handler) >>> 0, 1); @@ -70,7 +70,7 @@ export default function mitt(all?: EventHandlerMap): Emitter { * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler * @memberOf mitt */ - emit(type: EventType, evt: any) { + emit(type: EventType, evt: T) { ((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); }); ((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); }); } diff --git a/test/index.js b/test/index.js deleted file mode 100644 index a837d36..0000000 --- a/test/index.js +++ /dev/null @@ -1,182 +0,0 @@ -import mitt from '..'; -import chai, { expect } from 'chai'; -import { spy } from 'sinon'; -import sinonChai from 'sinon-chai'; -chai.use(sinonChai); - -describe('mitt', () => { - it('should default export be a function', () => { - expect(mitt).to.be.a('function'); - }); - - it('should accept an optional event handler map', () => { - expect(() => mitt(new Map())).not.to.throw; - const map = new Map(); - const a = spy(); - const b = spy(); - map.set('foo', [a, b]); - const events = mitt(map); - events.emit('foo'); - expect(a).to.have.been.calledOnce; - expect(b).to.have.been.calledOnce; - }); -}); - -describe('mitt#', () => { - let events, inst; - - beforeEach( () => { - events = new Map(); - inst = mitt(events); - }); - - describe('on()', () => { - it('should be a function', () => { - expect(inst) - .to.have.property('on') - .that.is.a('function'); - }); - - it('should register handler for new type', () => { - const foo = () => {}; - inst.on('foo', foo); - - expect(events.get('foo')).to.deep.equal([foo]); - }); - - it('should register handlers for any type strings', () => { - const foo = () => {}; - inst.on('constructor', foo); - - expect(events.get('constructor')).to.deep.equal([foo]); - }); - - it('should append handler for existing type', () => { - const foo = () => {}; - const bar = () => {}; - inst.on('foo', foo); - inst.on('foo', bar); - - expect(events.get('foo')).to.deep.equal([foo, bar]); - }); - - it('should NOT normalize case', () => { - const foo = () => {}; - inst.on('FOO', foo); - inst.on('Bar', foo); - inst.on('baz:baT!', foo); - - expect(events.get('FOO')).to.deep.equal([foo]); - expect(events.has('foo')).to.equal(false); - expect(events.get('Bar')).to.deep.equal([foo]); - expect(events.has('bar')).to.equal(false); - expect(events.get('baz:baT!')).to.deep.equal([foo]); - }); - - it('can take symbols for event types', () => { - const foo = () => {}; - const eventType = Symbol('eventType'); - inst.on(eventType, foo); - expect(events.get(eventType)).to.deep.equal([foo]); - }); - - // Adding the same listener multiple times should register it multiple times. - // See https://nodejs.org/api/events.html#events_emitter_on_eventname_listener - it('should add duplicate listeners', () => { - const foo = () => {}; - inst.on('foo', foo); - inst.on('foo', foo); - expect(events.get('foo')).to.deep.equal([foo, foo]); - }); - }); - - describe('off()', () => { - it('should be a function', () => { - expect(inst) - .to.have.property('off') - .that.is.a('function'); - }); - - it('should remove handler for type', () => { - const foo = () => {}; - inst.on('foo', foo); - inst.off('foo', foo); - - expect(events.get('foo')).to.be.empty; - }); - - it('should NOT normalize case', () => { - const foo = () => {}; - inst.on('FOO', foo); - inst.on('Bar', foo); - inst.on('baz:bat!', foo); - - inst.off('FOO', foo); - inst.off('Bar', foo); - inst.off('baz:baT!', foo); - - expect(events.get('FOO')).to.be.empty; - expect(events.has('foo')).to.equal(false); - expect(events.get('Bar')).to.be.empty; - expect(events.has('bar')).to.equal(false); - expect(events.get('baz:bat!')).to.have.lengthOf(1); - }); - - it('should remove only the first matching listener', () => { - const foo = () => {}; - inst.on('foo', foo); - inst.on('foo', foo); - inst.off('foo', foo); - expect(events.get('foo')).to.deep.equal([foo]); - inst.off('foo', foo); - expect(events.get('foo')).to.deep.equal([]); - }); - }); - - describe('emit()', () => { - it('should be a function', () => { - expect(inst) - .to.have.property('emit') - .that.is.a('function'); - }); - - it('should invoke handler for type', () => { - const event = { a: 'b' }; - - inst.on('foo', (one, two) => { - expect(one).to.deep.equal(event); - expect(two).to.be.an('undefined'); - }); - - inst.emit('foo', event); - }); - - it('should NOT ignore case', () => { - const onFoo = spy(), - onFOO = spy(); - events.set('Foo', [onFoo]); - events.set('FOO', [onFOO]); - - inst.emit('Foo', 'Foo arg'); - inst.emit('FOO', 'FOO arg'); - - expect(onFoo).to.have.been.calledOnce.and.calledWith('Foo arg'); - expect(onFOO).to.have.been.calledOnce.and.calledWith('FOO arg'); - }); - - it('should invoke * handlers', () => { - const star = spy(), - ea = { a: 'a' }, - eb = { b: 'b' }; - - events.set('*', [star]); - - inst.emit('foo', ea); - expect(star).to.have.been.calledOnce.and.calledWith('foo', ea); - star.resetHistory(); - - inst.emit('bar', eb); - expect(star).to.have.been.calledOnce.and.calledWith('bar', eb); - }); - }); -}); diff --git a/test/index_test.ts b/test/index_test.ts new file mode 100644 index 0000000..1f1d9bb --- /dev/null +++ b/test/index_test.ts @@ -0,0 +1,182 @@ +import mitt, { Emitter } from '..'; +import chai, { expect } from 'chai'; +import { spy } from 'sinon'; +import sinonChai from 'sinon-chai'; +chai.use(sinonChai); + +describe('mitt', () => { + it('should default export be a function', () => { + expect(mitt).to.be.a('function'); + }); + + it('should accept an optional event handler map', () => { + expect(() => mitt(new Map())).not.to.throw; + const map = new Map(); + const a = spy(); + const b = spy(); + map.set('foo', [a, b]); + const events = mitt(map); + events.emit('foo'); + expect(a).to.have.been.calledOnce; + expect(b).to.have.been.calledOnce; + }); +}); + +describe('mitt#', () => { + let events, inst: Emitter; + + beforeEach( () => { + events = new Map(); + inst = mitt(events); + }); + + describe('on()', () => { + it('should be a function', () => { + expect(inst) + .to.have.property('on') + .that.is.a('function'); + }); + + it('should register handler for new type', () => { + const foo = () => {}; + inst.on('foo', foo); + + expect(events.get('foo')).to.deep.equal([foo]); + }); + + it('should register handlers for any type strings', () => { + const foo = () => {}; + inst.on('constructor', foo); + + expect(events.get('constructor')).to.deep.equal([foo]); + }); + + it('should append handler for existing type', () => { + const foo = () => {}; + const bar = () => {}; + inst.on('foo', foo); + inst.on('foo', bar); + + expect(events.get('foo')).to.deep.equal([foo, bar]); + }); + + it('should NOT normalize case', () => { + const foo = () => {}; + inst.on('FOO', foo); + inst.on('Bar', foo); + inst.on('baz:baT!', foo); + + expect(events.get('FOO')).to.deep.equal([foo]); + expect(events.has('foo')).to.equal(false); + expect(events.get('Bar')).to.deep.equal([foo]); + expect(events.has('bar')).to.equal(false); + expect(events.get('baz:baT!')).to.deep.equal([foo]); + }); + + it('can take symbols for event types', () => { + const foo = () => {}; + const eventType = Symbol('eventType'); + inst.on(eventType, foo); + expect(events.get(eventType)).to.deep.equal([foo]); + }); + + // Adding the same listener multiple times should register it multiple times. + // See https://nodejs.org/api/events.html#events_emitter_on_eventname_listener + it('should add duplicate listeners', () => { + const foo = () => {}; + inst.on('foo', foo); + inst.on('foo', foo); + expect(events.get('foo')).to.deep.equal([foo, foo]); + }); + }); + + describe('off()', () => { + it('should be a function', () => { + expect(inst) + .to.have.property('off') + .that.is.a('function'); + }); + + it('should remove handler for type', () => { + const foo = () => {}; + inst.on('foo', foo); + inst.off('foo', foo); + + expect(events.get('foo')).to.be.empty; + }); + + it('should NOT normalize case', () => { + const foo = () => {}; + inst.on('FOO', foo); + inst.on('Bar', foo); + inst.on('baz:bat!', foo); + + inst.off('FOO', foo); + inst.off('Bar', foo); + inst.off('baz:baT!', foo); + + expect(events.get('FOO')).to.be.empty; + expect(events.has('foo')).to.equal(false); + expect(events.get('Bar')).to.be.empty; + expect(events.has('bar')).to.equal(false); + expect(events.get('baz:bat!')).to.have.lengthOf(1); + }); + + it('should remove only the first matching listener', () => { + const foo = () => {}; + inst.on('foo', foo); + inst.on('foo', foo); + inst.off('foo', foo); + expect(events.get('foo')).to.deep.equal([foo]); + inst.off('foo', foo); + expect(events.get('foo')).to.deep.equal([]); + }); + }); + + describe('emit()', () => { + it('should be a function', () => { + expect(inst) + .to.have.property('emit') + .that.is.a('function'); + }); + + it('should invoke handler for type', () => { + const event = { a: 'b' }; + + inst.on('foo', (one, two?) => { + expect(one).to.deep.equal(event); + expect(two).to.be.an('undefined'); + }); + + inst.emit('foo', event); + }); + + it('should NOT ignore case', () => { + const onFoo = spy(), + onFOO = spy(); + events.set('Foo', [onFoo]); + events.set('FOO', [onFOO]); + + inst.emit('Foo', 'Foo arg'); + inst.emit('FOO', 'FOO arg'); + + expect(onFoo).to.have.been.calledOnce.and.calledWith('Foo arg'); + expect(onFOO).to.have.been.calledOnce.and.calledWith('FOO arg'); + }); + + it('should invoke * handlers', () => { + const star = spy(), + ea = { a: 'a' }, + eb = { b: 'b' }; + + events.set('*', [star]); + + inst.emit('foo', ea); + expect(star).to.have.been.calledOnce.and.calledWith('foo', ea); + star.resetHistory(); + + inst.emit('bar', eb); + expect(star).to.have.been.calledOnce.and.calledWith('bar', eb); + }); + }); +}); diff --git a/test/test-types-compilation.ts b/test/test-types-compilation.ts new file mode 100644 index 0000000..00510da --- /dev/null +++ b/test/test-types-compilation.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */ + +import mitt from '..'; + +const emitter = mitt(); + +/* + * Check that if on is provided a generic, it only accepts handlers of that type + */ +{ + const badHandler = (x: number) => {}; + const goodHandler = (x: string) => {}; + + // @ts-expect-error + emitter.on('foo', badHandler); + emitter.on('foo', goodHandler); +} + +/* + * Check that if off is provided a generic, it only accepts handlers of that type + */ +{ + const badHandler = (x: number) => {}; + const goodHandler = (x: string) => {}; + + // @ts-expect-error + emitter.off('foo', badHandler); + emitter.off('foo', goodHandler); +} + + +/* + * Check that if emitt is provided a generic, it only accepts event data of that type + */ +{ + interface SomeEventData { + name: string; + } + // @ts-expect-error + emitter.emit('foo', 'NOT VALID'); + emitter.emit('foo', { name: 'jack' }); +} + diff --git a/test/types.ts b/test/types.ts deleted file mode 100644 index 23334bb..0000000 --- a/test/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import mitt, { EventHandlerList, EventHandlerMap } from '..'; - -const events = mitt(); -function foo() {} -events.on('foo', foo); -events.emit('foo', 'hello'); - -// handler return type should be ignored: -events.on('foo', async e => e * 42); - -// event map type -const map = new Map([ - ['foo', [foo]] -]); -const events2 = mitt(map); -events2.emit('foo', 'hello'); - -// event map type & iterables -const map2 : EventHandlerMap = new Map(Object.entries(({ foo: [foo] }))); -mitt(map2); diff --git a/tsconfig.json b/tsconfig.json index 2610831..acab4f5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,11 @@ "compilerOptions": { "noEmit": true, "declaration": true, - "moduleResolution": "node" + "moduleResolution": "node", + "esModuleInterop": true }, - "exclude": [ - "test" + "include": [ + "src/*.ts", + "test/*.ts", ] } -- cgit v1.2.3