diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3207f3..421de67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,7 @@ A lot of annyang's functionality came from pull requests sent over GitHub. Here - [x] Run grunt to make sure everything runs smoothly `$ grunt` - [x] Add tests for your code. [See details below](#automated-testing). - [x] Code, code, code. Changes should be done in `/src/annyang.js`. They will be transpiled to `/dist/annyang.js` and `/dist/annyang.min.js`. +- [x] If you add new or modify existing functions or externally facing data structures, update the typescript definition file under `types/index.d.ts` and add test code for dts linting at `types/annyang-tests.ts` - [x] Run `$ grunt` after making changes to verify that everything still works and the tests all pass. :bulb: A great alternative to repeatedly running `$ grunt` is to run `$ grunt watch` once, and leave this process running. It will continuously run all the tests and build the files every time you make a change to one of annyang's files. It will even *beep* if you make an error, and help you debug it. :+1: @@ -56,6 +57,8 @@ The tests reside in *BasicSpec.js*. The file contains a series of spec groups (e To simulate Speech Recognition in the testing environment, annyang uses a mock object called [Corti](https://github.com/TalAter/Corti) which mocks the browser's SpeechRecognition object. Corti also adds a number of utility functions to the SpeechRecognition object which simulate user actions (e.g. `say('Hello there')`), and allow checking the SpeechRecognition's status (e.g. `isListening() === true`). +annyang also has a typescript declaration file under `types/` and this is tested using [dtslint](https://github.com/Microsoft/dtslint). + ### Reporting Bugs Bugs are tracked as [GitHub issues](https://github.com/TalAter/annyang/issues). If you found a bug with annyang, the quickest way to get help would be to look through existing open and closed [GitHub issues](https://github.com/TalAter/annyang/issues?q=is%3Aissue). If the issue is already being discussed and hasn't been resolved yet, you can join the discussion and provide details about the problem you are having. If this is a new bug, please open a [new issue](https://github.com/TalAter/annyang/issues/new). diff --git a/Gruntfile.js b/Gruntfile.js index f78c87f..82254bd 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -128,6 +128,11 @@ module.exports = function(grunt) { } } } + }, + exec: { + 'types-test': { + command: 'npm run types-test' + } } }); @@ -137,7 +142,7 @@ module.exports = function(grunt) { }); // Register tasks - grunt.registerTask('default', ['jshint', 'babel', 'uglify', 'cssmin', 'jasmine', 'markdox']); + grunt.registerTask('default', ['jshint', 'babel', 'uglify', 'cssmin', 'jasmine', 'exec', 'markdox']); grunt.registerTask('dev', ['default', 'connect', 'watch']); grunt.registerTask('test', ['default']); diff --git a/package.json b/package.json index d7b29f7..1cbfc79 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "A javascript library for adding voice commands to your site, using speech recognition", "homepage": "https://www.talater.com/annyang/", "main": "dist/annyang.min.js", + "types": "types", "repository": { "type": "git", "url": "https://github.com/TalAter/annyang.git" @@ -24,12 +25,14 @@ }, "scripts": { "start": "grunt dev", - "test": "grunt test" + "test": "grunt test", + "types-test": "dtslint types" }, "devDependencies": { "async": "^2.4.1", "babel-core": "^6.25.0", "babel-preset-env": "^1.6.0", + "dtslint": "Microsoft/dtslint#production", "grunt": "~0.4.5", "grunt-babel": "^7.0.0", "grunt-cli": "^1.2.0", @@ -40,6 +43,7 @@ "grunt-contrib-jshint": "^1.1.0", "grunt-contrib-uglify": "^3.0.1", "grunt-contrib-watch": "^1.0.0", + "grunt-exec": "^3.0.0", "grunt-markdox": "^1.2.1", "grunt-template-jasmine-istanbul": "^0.5.0", "grunt-template-jasmine-requirejs": "^0.2.3", diff --git a/types/annyang-tests.ts b/types/annyang-tests.ts new file mode 100644 index 0000000..dca353b --- /dev/null +++ b/types/annyang-tests.ts @@ -0,0 +1,71 @@ +import { Annyang, CommandOption } from 'annyang'; + +declare let annyang: Annyang; +declare let console: any; + +// Tests based on API documentation at https://github.com/TalAter/annyang/blob/master/docs/README.md + +function testStartListening() { + annyang.start({ autoRestart: false }); // $ExpectType void + annyang.start({ autoRestart: false, continuous: false }); // $ExpectType void +} + +function testAddComments() { + const helloFunction = (): string => { + return 'hello'; + }; + + const commands: CommandOption = {'hello :name': helloFunction, howdy: helloFunction}; + const commands2: CommandOption = {hi: helloFunction}; + + annyang.addCommands(commands); // $ExpectType void + annyang.addCommands(commands2); // $ExpectType void + annyang.removeCommands(); // $ExpectType void + annyang.addCommands(commands); // $ExpectType void + annyang.removeCommands('hello'); // $ExpectType void + annyang.removeCommands(['howdy', 'hi']); // $ExpectType void +} + +const notConnected = () => { console.error('network connection error'); }; + +function testAddCallback() { + annyang.addCallback('error', () => console.error('There was an error!')); // $ExpectType void + + // $ExpectType void + annyang.addCallback('resultMatch', (userSaid, commandText, phrases) => { + console.log(userSaid); + console.log(commandText); + console.log(phrases); + }); + + annyang.addCallback('errorNetwork', notConnected, annyang); // $ExpectType void +} + +function testRemoveCallback() { + const start = () => { console.log('start'); }; + const end = () => { console.log('end'); }; + + annyang.addCallback('start', start); // $ExpectType void + annyang.addCallback('end', end); // $ExpectType void + + annyang.removeCallback(); // $ExpectType void + + annyang.removeCallback('end'); // $ExpectType void + + annyang.removeCallback('start', start); // $ExpectType void + + annyang.removeCallback(undefined, start); // $ExpectType void +} + +function testTrigger() { + annyang.trigger('Time for some thrilling heroics'); + + // $ExpectType void + annyang.trigger( + ['Time for some thrilling heroics', 'Time for some thrilling aerobics'] + ); +} + +function testIsListening() { + annyang.isListening(); // $ExpectType boolean +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..63a7c9a --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,220 @@ +declare namespace annyang { + /** + * Options for function `start` + * + * @export + * @interface StartOptions + */ + interface StartOptions { + /** + * Should annyang restart itself if it is closed indirectly, because of silence or window conflicts? + * + * @type {boolean} + */ + autoRestart?: boolean; + /** + * Allow forcing continuous mode on or off. Annyang is pretty smart about this, so only set this if you know what you're doing. + * + * @type {boolean} + */ + continuous?: boolean; + } + + /** + * A command option that supports custom regular expressions + * + * @export + * @interface CommandOptionRegex + */ + interface CommandOptionRegex { + /** + * @type {RegExp} + */ + regexp: RegExp; + /** + * @type {() => any} + */ + callback(): void; + } + + /** + * Commands that annyang should listen to + * + * #### Examples: + * ````javascript + * {'hello :name': helloFunction, 'howdy': helloFunction}; + * {'hi': helloFunction}; + * ```` + * @export + * @interface CommandOption + */ + interface CommandOption { + [command: string]: CommandOptionRegex | (() => void); + } + + /** + * Supported Events that will be triggered to listeners, you attach using `annyang.addCallback()` + * + * `start` - Fired as soon as the browser's Speech Recognition engine starts listening + * `error` - Fired when the browser's Speech Recogntion engine returns an error, this generic error callback will be followed by more accurate error callbacks (both will fire if both are defined) + * `errorNetwork` - Fired when Speech Recognition fails because of a network error + * `errorPermissionBlocked` - Fired when the browser blocks the permission request to use Speech Recognition. + * `errorPermissionDenied` - Fired when the user blocks the permission request to use Speech Recognition. + * `end` - Fired when the browser's Speech Recognition engine stops + * `result` - Fired as soon as some speech was identified. This generic callback will be followed by either the `resultMatch` or `resultNoMatch` callbacks. + * Callback functions registered to this event will include an array of possible phrases the user said as the first argument + * `resultMatch` - Fired when annyang was able to match between what the user said and a registered command + * Callback functions registered to this event will include three arguments in the following order: + * * The phrase the user said that matched a command + * * The command that was matched + * * An array of possible alternative phrases the user might've said + * `resultNoMatch` - Fired when what the user said didn't match any of the registered commands. + * Callback functions registered to this event will include an array of possible phrases the user might've said as the first argument + */ + type Events = + 'start' | + 'soundstart' | + 'error' | + 'end' | + 'result' | + 'resultMatch' | + 'resultNoMatch' | + 'errorNetwork' | + 'errorPermissionBlocked' | + 'errorPermissionDenied'; + + interface Annyang { + /** + * Start listening. + * It's a good idea to call this after adding some commands first, but not mandatory. + * + * @param {StartOptions} options + */ + start(options?: StartOptions): void; + + /** + * Stop listening, and turn off mic. + * + */ + abort(): void; + + /** + * Pause listening. annyang will stop responding to commands (until the resume or start methods are called), without turning off the browser's SpeechRecognition engine or the mic. + * + */ + pause(): void; + + /** + * Resumes listening and restores command callback execution when a result matches. + * If SpeechRecognition was aborted (stopped), start it. + * + */ + resume(): void; + + /** + * Turn on output of debug messages to the console. Ugly, but super-handy! + * + * @export + * @param {boolean} [newState=true] Turn on/off debug messages + */ + debug(newState?: boolean): void; + + /** + * Set the language the user will speak in. If this method is not called, defaults to 'en-US'. + * + * @param {string} lang + * @see [Languages](https://github.com/TalAter/annyang/blob/master/docs/FAQ.md#what-languages-are-supported) + */ + setLanguage(lang: string): void; + + /** + * Add commands that annyang will respond to. Similar in syntax to init(), but doesn't remove existing commands. + * + * #### Examples: + * ````javascript + * var commands = {'hello :name': helloFunction, 'howdy': helloFunction}; + * var commands2 = {'hi': helloFunction}; + * + * annyang.addCommands(commands); + * annyang.addCommands(commands2); + * // annyang will now listen to all three commands + * ```` + * + * @param {CommandOption} commands + */ + addCommands(commands: CommandOption): void; + + /** + * Removes all existing commands, a list of commands, or a specific command + * #### Examples: + * ````javascript + * var commands : annyang.CommandOption = {'hello': helloFunction, 'howdy': helloFunction, 'hi': helloFunction}; + * // Add some commands + * annyang.addCommands(commands); + * // Don't respond to howdy or hi + * annyang.removeCommands(['howdy', 'hi']); + * + * // Don't respond to hello + * annyang.removeCommands('hello'); + * + * // Remove all existing commands + * annyang.removeCommands(); + * ```` + * + * @param {string | string[]} command + */ + removeCommands(command?: string | string[]): void; + + /** + * @param {Events} event + * @param {(userSaid : string, commandText : string, results : string[]) => void} callback + * @param {*} [context] + */ + addCallback(event: Events, callback: (userSaid?: string, commandText?: string, results?: string[]) => void, context?: any): void; + + /** + * @param {Events} [event] + * @param {Function} [callback] + */ + removeCallback(event?: Events, callback?: (userSaid: string, commandText: string, results: string[]) => void): void; + + /** + * Returns true if speech recognition is currently on. + * Returns false if speech recognition is off or annyang is paused. + * + * @returns {boolean} + */ + isListening(): boolean; + + /** + * Returns the instance of the browser's SpeechRecognition object used by annyang. + * Useful in case you want direct access to the browser's Speech Recognition engine. + * + * @returns {*} + */ + getSpeechRecognizer(): any; + + /** + * Simulate speech being recognized. This will trigger the same events and behavior as when the Speech Recognition + * detects speech. + * + * Can accept either a string containing a single sentence, or an array containing multiple sentences to be checked + * in order until one of them matches a command (similar to the way Speech Recognition Alternatives are parsed) + * + * #### Examples: + * ````javascript + * annyang.trigger('Time for some thrilling heroics'); + * annyang.trigger( + * ['Time for some thrilling heroics', 'Time for some thrilling aerobics'] + * ); + * ```` + * + * @param {string} command + */ + trigger(command: string | string[]): void; + } +} + +declare var annyang: annyang.Annyang; +export = annyang; +export as namespace annyang; diff --git a/types/tsconfig.json b/types/tsconfig.json new file mode 100644 index 0000000..a9cf1d2 --- /dev/null +++ b/types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "noEmit": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { "annyang": ["."] } + }, + "files": [ + "index.d.ts", + "annyang-tests.ts" + ] +} diff --git a/types/tslint.json b/types/tslint.json new file mode 100644 index 0000000..c17ac4d --- /dev/null +++ b/types/tslint.json @@ -0,0 +1 @@ +{ "extends": "dtslint/dtslint.json" }