diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..d2233c8a --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,39 @@ + +{ + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true, + "modules": true, + "experimentalObjectRestSpread": true + } + }, + "plugins": [ + "react" + ], + "env": { + "browser": true, + "node": true, + "es6": true + }, + "extends": ["eslint:recommended", "plugin:react/recommended"], + "rules": { + "semi": ["error", "always"], + "quotes": ["off", "double"], + "no-tabs": "off", + "new-cap": "off", + "no-unused-vars": ["error", {"args": "none"}], + "no-console": ["error", { "allow": ["warn", "error", "log"] }], + "react/no-find-dom-node": "warn", + "react/no-string-refs": "warn" + + }, + "settings": { + "react": { + "pragma": "React", + "version": "15.6.1" + } + } +} \ No newline at end of file diff --git a/.flowconfig b/.flowconfig index b05ff1ab..1db8805b 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,4 +1,5 @@ [ignore] +.*node_modules/config-chain/test/* .*node_modules/flow-bin.* .*node_modules/jsxhint.* .*node_modules/.*mocha.* @@ -30,7 +31,7 @@ [libs] lib -types node_modules/data-canvas/flowtype [options] +suppress_comment= \\(.\\|\n\\)*\\$FlowIgnore diff --git a/.travis.yml b/.travis.yml index 024a71f3..c0824a37 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ sudo: false # Use container-based infrastructure language: node_js node_js: - - "5.1" + - "6.14" script: > npm run build && diff --git a/lib/q.js b/lib/q.js index 6b042d2e..98a04906 100644 --- a/lib/q.js +++ b/lib/q.js @@ -23,6 +23,7 @@ declare module "q" { promise: Promise; resolve(value: T): void; reject(reason: any): void; + delay(time: number):void; notify(value: any): void; makeNodeResolver(): (reason: any, value: T) => void; } @@ -44,7 +45,7 @@ declare module "q" { /** * The then method from the Promises/A+ specification, with an additional progress handler. */ - then(onFulfill?: (value: T) => U | IPromise, onReject?: (error: any) => U | IPromise, onProgress?: Function): Promise; + then(onFulfill?: (value: T) => U | IPromise | Promise, onReject?: (error: any) => U | IPromise, onProgress?: Function): Promise; /** * Like then, but "spreads" the array into a variadic fulfillment handler. If any of the promises in the array are rejected, instead calls onRejected with the first rejected promise's rejection reason. @@ -164,13 +165,13 @@ declare module "q" { } // If no value provided, returned promise will be of void type - declare function when(): Promise; + declare function when(): Promise; // if no fulfill, reject, or progress provided, returned promise will be of same type - declare function when(value: T | IPromise): Promise; + declare function when(value: T | IPromise,): Promise; // If a non-promise value is provided, it will not reject or progress - declare function when(value: T | IPromise, onFulfilled: (val: T) => U | IPromise, onRejected?: (reason: any) => U | IPromise, onProgress?: (progress: any) => any): Promise; + declare function when(value: T | IPromise, onFulfilled: (val: T) => U | IPromise, onRejected?: (reason: any) => U | IPromise, onProgress?: (progress: any) => any, ...rest: Array): Promise; /** * Currently "impossible" (and I use the term loosely) to implement due to TypeScript limitations as it is now. @@ -200,8 +201,10 @@ declare module "q" { /** * Returns a promise that is fulfilled with an array containing the fulfillment value of each promise, or is rejected with the same rejection reason as the first promise to be rejected. */ - declare function all(promises: [Promise, Promise, Promise]): Promise<[A, B, C]>; - declare function all(promises: [Promise, Promise]): Promise<[A, B]>; + declare function all(promises: Promise[]): Promise; // catch all + // declare function all(promises: [Promise, Promise, Promise, Promise]): Promise<[A, B, C, D]>; + // declare function all(promises: [Promise, Promise, Promise]): Promise<[A, B, C]>; + // declare function all(promises: [Promise, Promise]): Promise<[A, B]>; /** * Returns a promise that is fulfilled with an array of promise state snapshots, but only after all the original promises have settled, i.e. become either fulfilled or rejected. @@ -314,4 +317,6 @@ declare module "q" { * Calling resolve with a non-promise value causes promise to be fulfilled with that value. */ declare function resolve(object: T): Promise; + + declare function delay(time: number): Promise; } diff --git a/lib/underscore.js b/lib/underscore.js index 6f415a15..76933b30 100644 --- a/lib/underscore.js +++ b/lib/underscore.js @@ -6,7 +6,7 @@ declare module "underscore" { declare function clone(obj: T): T; declare function isEqual(a: S, b: T): boolean; - declare function range(a: number, b: number): Array; + declare function range(a?: number, b: number, c?: number): Array; declare function extend(o1: S, o2: T): S & T; declare function zip(a1: S[], a2: T[]): Array<[S, T]>; @@ -41,11 +41,12 @@ declare module "underscore" { declare function groupBy(a: Array, iteratee: (val: T, index: number)=>K): {[key:K]: T[]}; - declare function min(a: Array|{[key:any]: T}): T; + declare function min(a: Array|{[key:any]: T}, iteratee: (val: T)=>any): T; declare function max(a: Array|{[key:any]: T}): T; declare function values(o: {[key: any]: T}): T[]; declare function flatten(a: Array): Array; + declare function flatten(a: Array, shallow?: boolean): Array; declare function reduce(a: T[], fn: (memo: R, val: T, idx: number, list: T[])=>R, memo?: R): R; declare function reduce(a: {[key:K]: T}, fn: (memo: R, val: T, key: K, o: {[key:K]: T})=>R, memo?: R): R; diff --git a/package.json b/package.json index 9623c7ca..70b07f04 100644 --- a/package.json +++ b/package.json @@ -65,14 +65,22 @@ "devDependencies": { "arraybuffer-slice": "^0.1.2", "babel": "^5.8.23", + "babel-eslint" : "8.2.6", "babel-core": "^5.8.23", "babelify": "^6.3.0", "browserify": "^10.2.4", "chai": "^2.0.0", "coveralls": "2.10.x", "envify": "^3.4.0", + "eslint": "^5.4.0", + "eslint-config-standard": "^11.0.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-node": "^7.0.1", + "eslint-plugin-promise": "^4.0.0", + "eslint-plugin-standard": "^3.1.0", + "eslint-plugin-react": "7.11.1", "exorcist": "^0.4.0", - "flow-bin": "^0.21.0", + "flow-bin": "^0.79.1", "http-server": "^0.8.0", "istanbul": "^0.3.17", "jsxhint": "git://github.com/strml/JSXHint.git", diff --git a/scripts/lint.sh b/scripts/lint.sh index 326507f0..d7b9fee8 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -19,4 +19,4 @@ if [[ $package_version != $code_version ]]; then fi # Run the usual linter -./node_modules/.bin/jsxhint --es6module --harmony 'src/main/**/*.js' 'src/test/**/*.js' +./node_modules/.bin/eslint src/**/**/*.js diff --git a/src/main/Alignment.js b/src/main/Alignment.js index 7130a975..4cc68d8d 100644 --- a/src/main/Alignment.js +++ b/src/main/Alignment.js @@ -2,7 +2,9 @@ * Interface for alignments, shared between BAM and GA4GH backends. * @flow */ - +'use strict'; + +import type {GenomeRange} from './types'; import type ContigInterval from './ContigInterval'; // "CIGAR" operations express how a sequence aligns to the reference: does it @@ -13,6 +15,11 @@ export type CigarOp = { length: number } +// converts a string into a Strand element. Must be '+' or '-'. Any other +// strings will be converted to '.'. +function strToStrand(str: string): Strand { + return str && str == '+' ? '+' : (str && str == '-' ? '-' : '.'); // either +, - or . +} export type Strand = '-' | '+' | '.'; @@ -46,3 +53,7 @@ export type AlignmentDataSource = { once: (event: string, handler: Function) => void; off: (event: string) => void; }; + +module.exports = { + strToStrand +}; diff --git a/src/main/ContigInterval.js b/src/main/ContigInterval.js index ae6acc73..2a84b694 100644 --- a/src/main/ContigInterval.js +++ b/src/main/ContigInterval.js @@ -1,6 +1,8 @@ /* @flow */ 'use strict'; +import type {GenomeRange} from './types'; + import Interval from './Interval'; import {flatMap} from './utils'; @@ -108,10 +110,10 @@ class ContigInterval { } // Comparator for use with Array.prototype.sort - static compare(a: ContigInterval, b: ContigInterval): number { - if (a.contig > b.contig) { + static compare(a: ContigInterval, b: ContigInterval): number { + if (a.contig.toString() > b.contig.toString()) { return -1; - } else if (a.contig < b.contig) { + } else if (a.contig.toString() < b.contig.toString()) { return +1; } else { return a.start() - b.start(); @@ -120,7 +122,7 @@ class ContigInterval { // Sort an array of intervals & coalesce adjacent/overlapping ranges. // NB: this may re-order the intervals parameter - static coalesce(intervals: ContigInterval[]): ContigInterval[] { + static coalesce(intervals: ContigInterval[]): ContigInterval[] { intervals.sort(ContigInterval.compare); var rs = []; @@ -143,7 +145,7 @@ class ContigInterval { } static fromGenomeRange(range: GenomeRange): ContigInterval { - return new ContigInterval(range.contig, range.start, range.stop); + return new ContigInterval(range.contig.toString(), range.start, range.stop); } } diff --git a/src/main/Controls.js b/src/main/Controls.js index c07b5f34..f4e0d440 100644 --- a/src/main/Controls.js +++ b/src/main/Controls.js @@ -4,7 +4,7 @@ */ 'use strict'; -import type {PartialGenomeRange} from './types'; +import type {GenomeRange, PartialGenomeRange} from './types'; import React from 'react'; import _ from 'underscore'; @@ -18,7 +18,7 @@ type Props = { onChange: (newRange: GenomeRange)=>void; }; -class Controls extends React.Component { +class Controls extends React.Component { props: Props; state: void; // no state @@ -50,14 +50,14 @@ class Controls extends React.Component { if (altContig) range.contig = altContig; } - return (_.extend({}, this.props.range, range) : any); + return (_.extend(_.clone(this.props.range), range) : any); } - handleContigChange(e: SyntheticEvent) { + handleContigChange(e: SyntheticEvent<>) { this.props.onChange(this.completeRange({contig: this.refs.contig.value})); } - handleFormSubmit(e: SyntheticEvent) { + handleFormSubmit(e: SyntheticEvent<>) { e.preventDefault(); var range = this.completeRange(utils.parseRange(this.refs.position.value)); this.props.onChange(range); diff --git a/src/main/LocalStringFile.js b/src/main/LocalStringFile.js index 32cbf17e..5b99e675 100644 --- a/src/main/LocalStringFile.js +++ b/src/main/LocalStringFile.js @@ -24,7 +24,7 @@ class LocalStringFile extends AbstractFile { } } - getBytes(start: number, length: number): Q.Promise { + getBytes(start: number, length: number): Q.Promise { if (length < 0) { return Q.reject(`Requested <0 bytes (${length})`); } @@ -41,7 +41,7 @@ class LocalStringFile extends AbstractFile { } // Read the entire file -- not recommended for large files! - getAll(): Q.Promise { + getAll(): Q.Promise { var buf = this.getFromCache(0, this.fileLength - 1); return Q.when(buf); } diff --git a/src/main/Menu.js b/src/main/Menu.js index 27b9a1d2..a65750e5 100644 --- a/src/main/Menu.js +++ b/src/main/Menu.js @@ -30,10 +30,10 @@ type Props = { onSelect: (key: string) => void; }; -class Menu extends React.Component { +class Menu extends React.Component { props: Props; - clickHandler(idx: number, e: SyntheticMouseEvent) { + clickHandler(idx: number, e: SyntheticMouseEvent<>) { e.preventDefault(); var item = this.props.items[idx]; if (typeof(item) == 'string') return; // for flow diff --git a/src/main/RemoteFile.js b/src/main/RemoteFile.js index f96229b5..89ec10ee 100644 --- a/src/main/RemoteFile.js +++ b/src/main/RemoteFile.js @@ -150,14 +150,14 @@ class RemoteFile extends AbstractFile{ promiseXHR(xhr: XMLHttpRequest): Q.Promise<[any, Event]> { var url = this.url; var deferred = Q.defer(); - xhr.addEventListener('load', function(e) { + xhr.addEventListener('load', function(e: any) { if (this.status >= 400) { deferred.reject(`Request for ${url} failed with status: ${this.status} ${this.statusText}`); } else { deferred.resolve([this.response, e]); } }); - xhr.addEventListener('error', function(e) { + xhr.addEventListener('error', function(e: any) { deferred.reject(`Request for ${url} failed with status: ${this.status} ${this.statusText}`); }); this.numNetworkRequests++; diff --git a/src/main/RemoteRequest.js b/src/main/RemoteRequest.js index 16b9bc5d..e8df5fc9 100644 --- a/src/main/RemoteRequest.js +++ b/src/main/RemoteRequest.js @@ -71,14 +71,14 @@ class RemoteRequest { promiseXHR(xhr: XMLHttpRequest): Q.Promise<[any, Event]> { var url = this.url; var deferred = Q.defer(); - xhr.addEventListener('load', function(e) { + xhr.addEventListener('load', function(e: any) { if (this.status >= 400) { deferred.reject(`Request for ${url} failed with status: ${this.status} ${this.statusText}`); } else { deferred.resolve([this.response, e]); } }); - xhr.addEventListener('error', function(e) { + xhr.addEventListener('error', function(e: any) { deferred.reject(`Request for ${url} failed with status: ${this.status} ${this.statusText}`); }); this.numNetworkRequests++; diff --git a/src/main/Root.js b/src/main/Root.js index 2927dfb6..62899389 100644 --- a/src/main/Root.js +++ b/src/main/Root.js @@ -4,6 +4,7 @@ */ 'use strict'; +import type {GenomeRange} from './types'; import type {TwoBitSource} from './sources/TwoBitDataSource'; import type {VisualizedTrack, VizWithOptions} from './types'; @@ -18,14 +19,16 @@ type Props = { initialRange: GenomeRange; }; -class Root extends React.Component { +type State = { + contigList: string[]; + range: ?GenomeRange; + settingsMenuKey: ?string; + updateSize: boolean; +}; + +class Root extends React.Component { props: Props; - state: { - contigList: string[]; - range: ?GenomeRange; - settingsMenuKey: ?string; - updateSize: boolean; - }; + state: State; trackReactElements: Array; //it's an array of reactelement that are created for tracks constructor(props: Object) { @@ -68,7 +71,7 @@ class Root extends React.Component { }).done(); } - toggleSettingsMenu(key: string, e: SyntheticEvent) { + toggleSettingsMenu(key: string, e: SyntheticEvent<>) { if (this.state.settingsMenuKey == key) { this.setState({settingsMenuKey: null}); } else { @@ -78,9 +81,9 @@ class Root extends React.Component { handleSelectOption(trackKey: string, optionKey: string) { this.setState({settingsMenuKey: null}); - var viz = this.props.tracks[Number(trackKey)].visualization; var oldOpts = viz.options; + // $FlowIgnore: TODO remove flow suppression var newOpts = viz.component.handleSelectOption(optionKey, oldOpts); viz.options = newOpts; if (newOpts != oldOpts) { @@ -88,9 +91,9 @@ class Root extends React.Component { } } - makeDivForTrack(key: string, track: VisualizedTrack): React.Element { + makeDivForTrack(key: string, track: VisualizedTrack): React$Element<'div'> { //this should be improved, but I have no idea how (when trying to - //access this.trackReactElements wih string key, flow complains) + //access this.trackReactElements with string key, flow complains) var intKey = parseInt(key); var trackEl = ( {this.trackReactElements[intKey]=c}} + ref = {(c: React$ElementRef) => {this.trackReactElements[intKey]=c}} />); var trackName = track.track.name || '(track name)'; var gearIcon = null, settingsMenu = null; + // $FlowIgnore: TODO remove flow suppression if (track.visualization.component.getOptionsMenu) { gearIcon = ( diff --git a/src/main/VisualizationWrapper.js b/src/main/VisualizationWrapper.js index bb4810ab..63038341 100644 --- a/src/main/VisualizationWrapper.js +++ b/src/main/VisualizationWrapper.js @@ -5,7 +5,7 @@ import type {TwoBitSource} from './sources/TwoBitDataSource'; -import type {VizWithOptions} from './types'; +import type {GenomeRange, VizWithOptions} from './types'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -13,11 +13,12 @@ import d3utils from './viz/d3utils'; import _ from 'underscore'; import d3 from '../lib/minid3'; -export type VizProps = { +export type VizProps = { width: number; height: number; range: GenomeRange; referenceSource: TwoBitSource; + source: T; options: any; }; @@ -30,9 +31,15 @@ type Props = { options: ?Object; }; -class VisualizationWrapper extends React.Component { +type State = { + width: number; + height: number; + updateSize: boolean; +}; + +class VisualizationWrapper extends React.Component { props: Props; - state: {width: number; height: number; updateSize: boolean}; + state: State; hasDragBeenInitialized: boolean; onResizeListener: Object; //listener that handles window.onresize event @@ -47,12 +54,17 @@ class VisualizationWrapper extends React.Component { } updateSize(): any { - var parentDiv = ReactDOM.findDOMNode(this).parentNode; - this.setState({ - updateSize: false, - width: parentDiv.offsetWidth, - height: parentDiv.offsetHeight - }); + var thisNode = ReactDOM.findDOMNode(this) + if (thisNode && thisNode instanceof Element) { // check for getContext + var parentDiv = thisNode.parentNode; + if (parentDiv && parentDiv instanceof HTMLElement) { // check for getContext + this.setState({ + updateSize: false, + width: parentDiv.offsetWidth, + height: parentDiv.offsetHeight + }); + } + } } componentDidMount(): any { @@ -135,9 +147,13 @@ class VisualizationWrapper extends React.Component { const range = this.props.range; const component = this.props.visualization.component; if (!range) { - return ; + if (component.displayName != null) + return ; + else + return ; } - var options = _.extend({},this.props.visualization.options,this.props.options); + + var options = _.extend(_.clone(this.props.visualization.options),this.props.options); var el = React.createElement(component, ({ range: range, @@ -146,7 +162,7 @@ class VisualizationWrapper extends React.Component { width: this.state.width, height: this.state.height, options: options - } : VizProps)); + } : VizProps)); return
{el}
; } @@ -155,7 +171,7 @@ VisualizationWrapper.displayName = 'VisualizationWrapper'; type EmptyTrackProps = {className: string}; -class EmptyTrack extends React.Component { +class EmptyTrack extends React.Component { render() { var className = this.props.className + ' empty'; return
; diff --git a/src/main/data/SamRead.js b/src/main/data/SamRead.js index 61588747..b930fc66 100644 --- a/src/main/data/SamRead.js +++ b/src/main/data/SamRead.js @@ -216,7 +216,7 @@ class SamRead /* implements Alignment */ { return `Name: ${this.name} FLAG: ${this.getFlag()} -Position: ${this.getInterval()} +Position: ${this.getInterval().toString()} CIGAR: ${this.getCigarString()} Sequence: ${f.seq} Quality: ${this.getQualPhred()} diff --git a/src/main/data/TwoBit.js b/src/main/data/TwoBit.js index ca0231f4..a7f74891 100644 --- a/src/main/data/TwoBit.js +++ b/src/main/data/TwoBit.js @@ -148,7 +148,13 @@ function IncompleteChunkError(message) { this.name = "IncompleteChunkError"; this.message = (message || ""); } -IncompleteChunkError.prototype = Error.prototype; +IncompleteChunkError.prototype = Object.create(Error.prototype, { + constructor: { + value: IncompleteChunkError, + writable: true, + configurable: true + } +}); /** * Wraps a parsing attempt, captures errors related to @@ -238,6 +244,8 @@ class TwoBit { unpackDNA(dataView, start % 4, stop - start + 1), start, header) .join(''); }); + }).then(p => { + return p; }); } @@ -266,6 +274,8 @@ class TwoBit { }); } ); + }).then(p => { + return p; }); } } diff --git a/src/main/data/bam.js b/src/main/data/bam.js index f5a92393..66658187 100644 --- a/src/main/data/bam.js +++ b/src/main/data/bam.js @@ -42,11 +42,13 @@ var kMaxFetch = 65536 * 2; // Read a single alignment function readAlignment(view: jDataView, pos: number, - offset: VirtualOffset, refName: string) { + offset: VirtualOffset, refName: string): {read: SamRead, readLength: number } { var readLength = view.getInt32(pos); pos += 4; if (pos + readLength > view.byteLength) { + // We cannot replace with an error here, because promise depends on response. + // $FlowIgnore: TODO remove flow suppression. return null; } @@ -133,7 +135,7 @@ function fetchAlignments(remoteFile: RemoteFile, alignments = [], deferred = Q.defer(); - function fetch(chunks) { + function fetch(chunks: Chunk[]) { if (chunks.length === 0) { deferred.resolve(alignments); return; @@ -183,8 +185,7 @@ function fetchAlignments(remoteFile: RemoteFile, } else { newChunk = null; // This is most likely EOF } - - fetch((newChunk ? [newChunk] : []).concat(_.rest(chunks))); + fetch((newChunk !== null ? [newChunk] : []).concat(_.rest(chunks))); }); } @@ -254,20 +255,21 @@ class Bam { * This is insanely inefficient and should not be used outside of testing. */ readAtOffset(offset: VirtualOffset): Q.Promise { - return this.remoteFile.getBytes(offset.coffset, kMaxFetch).then(gzip => { - var buf = utils.inflateGzip(gzip); + var readPromise: Q.Promise = this.remoteFile.getBytes(offset.coffset, kMaxFetch).then(gzip => { + var buf: ArrayBuffer = utils.inflateGzip(gzip); var jv = new jDataView(buf, 0, buf.byteLength, true /* little endian */); var readData = readAlignment(jv, offset.uoffset, offset, ''); - if (!readData) { - throw `Unable to read alignment at ${offset} in ${this.remoteFile.url}`; - } else { - // Attach the human-readable ref name - var read = readData.read; - return this.header.then(header => { - read.ref = header.references[read.refID].name; - return read; - }); - } + + // Attach the human-readable ref name + var read: SamRead = readData.read; + return read; + }); + + + return Q.all([readPromise, this.header]) + .then(([read, header]) => { + read.ref = header.references[read.refID].name; + return read; }); } @@ -301,17 +303,20 @@ class Bam { var index = this.index; return this.getContigIndex(range.contig).then(({idx, name}) => { - var def = Q.defer(); + var def: Q.Deferred = Q.defer(); // This happens in the next event loop to give listeners a chance to register. Q.when().then(() => { def.notify({status: 'Fetching BAM index'}); }); var idxRange = new ContigInterval(idx, range.start(), range.stop()); - utils.pipePromise( - def, - index.getChunksForInterval(idxRange).then(chunks => { + var promise: Q.Promise = index.getChunksForInterval(idxRange).then(chunks => { return fetchAlignments(this.remoteFile, name, idxRange, contained, chunks); - })); + }).then(samReads => { + return samReads; + }); + + utils.pipePromise(def, promise); + return def.promise; }); } diff --git a/src/main/data/formats/bamTypes.js b/src/main/data/formats/bamTypes.js index 671ea07f..c064e2d0 100644 --- a/src/main/data/formats/bamTypes.js +++ b/src/main/data/formats/bamTypes.js @@ -48,7 +48,7 @@ var Flags = { SUPPLEMENTARY_ALIGNMENT: 0x800 }; -var ThickAlignment = _.extend({}, ThinAlignment, { +var ThickAlignment = _.extend(_.clone(ThinAlignment), { read_name: [nullString, 'l_read_name'], cigar: ['array', 'CigarOp', 'n_cigar_op'], seq: ['FourBitSequence', 'l_seq'], diff --git a/src/main/data/formats/helpers.js b/src/main/data/formats/helpers.js index 3f4f0b46..950f1d55 100644 --- a/src/main/data/formats/helpers.js +++ b/src/main/data/formats/helpers.js @@ -24,7 +24,7 @@ function typeAtOffset(baseType: any, offsetFieldName: string): any { // TODO: write this using 'binary' type (like nullString). var sizedBlock = jBinary.Type({ params: ['itemType', 'lengthField'], - resolve: function (getType) { + resolve: function(getType) { this.itemType = getType(this.itemType); }, read: function(context) { diff --git a/src/main/json/GA4GHAlignmentJson.js b/src/main/json/GA4GHAlignmentJson.js index 22113b43..7d95aa19 100644 --- a/src/main/json/GA4GHAlignmentJson.js +++ b/src/main/json/GA4GHAlignmentJson.js @@ -6,6 +6,7 @@ 'use strict'; import type {Alignment, AlignmentDataSource} from '../Alignment'; +import type {GenomeRange} from '../types'; import _ from 'underscore'; import {Events} from 'backbone'; @@ -45,7 +46,7 @@ function create(json: string): AlignmentDataSource { on: () => {}, once: () => {}, off: () => {}, - trigger: () => {} + trigger: (string, any) => {} }; _.extend(o, Events); return o; diff --git a/src/main/json/GA4GHFeatureJson.js b/src/main/json/GA4GHFeatureJson.js index c57593ee..c8c72670 100644 --- a/src/main/json/GA4GHFeatureJson.js +++ b/src/main/json/GA4GHFeatureJson.js @@ -12,6 +12,7 @@ import _ from 'underscore'; import {Events} from 'backbone'; import ContigInterval from '../ContigInterval'; +import type {GenomeRange} from '../types'; function create(json: string): FeatureDataSource { @@ -45,7 +46,7 @@ function create(json: string): FeatureDataSource { on: () => {}, once: () => {}, off: () => {}, - trigger: () => {} + trigger: (string, any) => {} }; _.extend(o, Events); return o; diff --git a/src/main/json/GA4GHVariantJson.js b/src/main/json/GA4GHVariantJson.js index b93519f6..bb8cfd59 100644 --- a/src/main/json/GA4GHVariantJson.js +++ b/src/main/json/GA4GHVariantJson.js @@ -12,6 +12,7 @@ import _ from 'underscore'; import {Events} from 'backbone'; import ContigInterval from '../ContigInterval'; +import type {GenomeRange} from '../types'; function create(json: string): VcfDataSource { @@ -45,7 +46,7 @@ function create(json: string): VcfDataSource { on: () => {}, once: () => {}, off: () => {}, - trigger: () => {} + trigger: (string, any) => {} }; _.extend(o, Events); return o; diff --git a/src/main/pileup.js b/src/main/pileup.js index 3f646679..10186279 100644 --- a/src/main/pileup.js +++ b/src/main/pileup.js @@ -6,7 +6,9 @@ import type {Track, VisualizedTrack, VizWithOptions} from './types'; import {AllelFrequencyStrategy} from './types'; +import type {State} from './types'; +import type {VizProps} from './VisualizationWrapper'; import _ from 'underscore'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -67,14 +69,15 @@ function findReference(tracks: VisualizedTrack[]): ?VisualizedTrack { function create(elOrId: string|Element, params: PileupParams): Pileup { var el = typeof(elOrId) == 'string' ? document.getElementById(elOrId) : elOrId; if (!el) { - throw new Error(`Attempted to create pileup with non-existent element ${elOrId}`); + throw new Error(`Attempted to create pileup with non-existent element ${elOrId.toString()}`); } var vizTracks = params.tracks.map(function(track: Track): VisualizedTrack { var source = track.data ? track.data : track.viz.component.defaultSource; if(!source) { + var displayName = track.viz.component.displayName != null ? track.viz.component.displayName : 'track'; throw new Error( - `Track '${track.viz.component.displayName}' doesn't have a default ` + + `Track '${displayName}' doesn't have a default ` + `data source; you must specify one when initializing it.` ); } @@ -95,8 +98,10 @@ function create(elOrId: string|Element, params: PileupParams): Pileup { //if the element doesn't belong to document DOM observe DOM to detect //when it's attached var observer = null; + var body = document.body; + if (!body) throw new Error("Failed to match: document.body"); - if (!document.body.contains(el)) { + if (!body.contains(el)) { observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { @@ -135,9 +140,14 @@ function create(elOrId: string|Element, params: PileupParams): Pileup { }, getRange(): GenomeRange { if (reactElement === null) { - throw 'Cannot call setRange on a destroyed pileup'; + throw 'Cannot call getRange on a destroyed pileup'; + } + if (reactElement.state.range != null) { + return _.clone(reactElement.state.range); + } else { + throw 'Cannot call setRange on non-existent range'; } - return _.clone(reactElement.state.range); + }, destroy(): void { if (!vizTracks) { @@ -161,8 +171,9 @@ function create(elOrId: string|Element, params: PileupParams): Pileup { type VizObject = ((options: ?Object) => VizWithOptions); -function makeVizObject(component: ReactClass): VizObject { +function makeVizObject(component: Class, State>>): VizObject { return options => { + // $FlowIgnore: TODO remove flow suppression options = _.extend({}, component.defaultOptions, options); return {component, options}; }; diff --git a/src/main/sources/BamDataSource.js b/src/main/sources/BamDataSource.js index b54863ce..2b13377f 100644 --- a/src/main/sources/BamDataSource.js +++ b/src/main/sources/BamDataSource.js @@ -9,6 +9,7 @@ import ContigInterval from '../ContigInterval'; import BamFile from '../data/bam'; import RemoteFile from '../RemoteFile'; +import type {GenomeRange} from '../types'; import type {Alignment, AlignmentDataSource} from '../Alignment'; // Genome ranges are rounded to multiples of this for fetching. @@ -108,7 +109,7 @@ function createFromBamFile(remoteSource: BamFile): AlignmentDataSource { on: () => {}, once: () => {}, off: () => {}, - trigger: () => {} + trigger: (status: string, param: any) => {} }; _.extend(o, Events); // Make this an event emitter diff --git a/src/main/sources/BigBedDataSource.js b/src/main/sources/BigBedDataSource.js index 5184d13c..d7590384 100644 --- a/src/main/sources/BigBedDataSource.js +++ b/src/main/sources/BigBedDataSource.js @@ -1,7 +1,9 @@ /* @flow */ 'use strict'; +import type {GenomeRange} from '../types'; import type {Strand} from '../Alignment'; +import {strToStrand} from '../Alignment'; import _ from 'underscore'; import Q from 'q'; @@ -29,7 +31,7 @@ export type Gene = { // Flow type for export. export type FeatureDataSource = { rangeChanged: (newRange: GenomeRange) => void; - getFeaturesInRange: (range: ContigInterval) => Feature[]; + getFeaturesInRange: (range: ContigInterval, resolution: ?number) => Feature[]; on: (event: string, handler: Function) => void; off: (event: string) => void; trigger: (event: string, ...args:any) => void; @@ -52,8 +54,8 @@ function parseBedFeature(f): Gene { // if no id, generate randomly for unique storage var id = x[0] ? x[0] : position.toString(); // e.g. ENST00000359597 - var score = x[1] ? x[1] : 1000; // number from 0-1000 - var strand = x[2] ? x[2] : '.'; // either +, - or . + var score = x[1] ? parseInt(x[1]) : 1000; // number from 0-1000 + var strand = strToStrand(x[2]); // either +, - or . var codingRegion = (x[3] && x[4]) ? new Interval(Number(x[3]), Number(x[4])) :new Interval(f.start, f.stop); var geneId = x[9] ? x[9] : id; var name = x[10] ? x[10] : ""; @@ -139,7 +141,7 @@ function createFromBigBedFile(remoteSource: BigBed): BigBedSource { // These are here to make Flow happy. on: () => {}, off: () => {}, - trigger: () => {} + trigger: (status: string, param: any) => {} }; _.extend(o, Events); // Make this an event emitter diff --git a/src/main/sources/EmptySource.js b/src/main/sources/EmptySource.js index 0892eb15..cd2893b4 100644 --- a/src/main/sources/EmptySource.js +++ b/src/main/sources/EmptySource.js @@ -4,6 +4,8 @@ */ 'use strict'; +import type {GenomeRange} from '../types'; + type EmptySource = { rangeChanged: (newRange: GenomeRange) => void; on: (event: string, handler: Function) => void; diff --git a/src/main/sources/GA4GHAlignmentSource.js b/src/main/sources/GA4GHAlignmentSource.js index c17f590f..2ed5f146 100644 --- a/src/main/sources/GA4GHAlignmentSource.js +++ b/src/main/sources/GA4GHAlignmentSource.js @@ -5,6 +5,7 @@ */ 'use strict'; +import type {GenomeRange} from '../types'; import type {Alignment, AlignmentDataSource} from '../Alignment'; import _ from 'underscore'; @@ -92,7 +93,7 @@ function create(spec: GA4GHSpec): AlignmentDataSource { xhr.responseType = 'json'; xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.addEventListener('load', function(e) { + xhr.addEventListener('load', function(e: any) { var response = this.response; if (this.status >= 400) { notifyFailure(this.status + ' ' + this.statusText + ' ' + JSON.stringify(response)); @@ -110,7 +111,7 @@ function create(spec: GA4GHSpec): AlignmentDataSource { } } }); - xhr.addEventListener('error', function(e) { + xhr.addEventListener('error', function(e: any) { notifyFailure('Request failed with status: ' + this.status); }); @@ -147,7 +148,7 @@ function create(spec: GA4GHSpec): AlignmentDataSource { on: () => {}, once: () => {}, off: () => {}, - trigger: () => {} + trigger: (status: string, param: any) => {} }; _.extend(o, Events); // Make this an event emitter return o; diff --git a/src/main/sources/GA4GHFeatureSource.js b/src/main/sources/GA4GHFeatureSource.js index aa805b55..43dcf117 100644 --- a/src/main/sources/GA4GHFeatureSource.js +++ b/src/main/sources/GA4GHFeatureSource.js @@ -6,7 +6,7 @@ import _ from 'underscore'; import {Events} from 'backbone'; - +import type {GenomeRange} from '../types'; import ContigInterval from '../ContigInterval'; import type {FeatureDataSource} from './BigBedDataSource'; @@ -77,7 +77,7 @@ function create(spec: GA4GHFeatureSpec): FeatureDataSource { xhr.responseType = 'json'; xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.addEventListener('load', function(e) { + xhr.addEventListener('load', function(e: any) { var response = this.response; if (this.status >= 400) { notifyFailure(this.status + ' ' + this.statusText + ' ' + JSON.stringify(response)); @@ -95,7 +95,7 @@ function create(spec: GA4GHFeatureSpec): FeatureDataSource { } } }); - xhr.addEventListener('error', function(e) { + xhr.addEventListener('error', function(e: any) { notifyFailure('Request failed with status: ' + this.status); }); @@ -123,7 +123,7 @@ function create(spec: GA4GHFeatureSpec): FeatureDataSource { on: () => {}, once: () => {}, off: () => {}, - trigger: () => {} + trigger: (status: string, param: any) => {} }; _.extend(o, Events); // Make this an event emitter return o; diff --git a/src/main/sources/GA4GHVariantSource.js b/src/main/sources/GA4GHVariantSource.js index ae96124e..96f1b594 100644 --- a/src/main/sources/GA4GHVariantSource.js +++ b/src/main/sources/GA4GHVariantSource.js @@ -6,7 +6,7 @@ import _ from 'underscore'; import {Events} from 'backbone'; - +import type {GenomeRange} from '../types'; import ContigInterval from '../ContigInterval'; import type {VcfDataSource} from './VcfDataSource'; @@ -78,7 +78,7 @@ function create(spec: GA4GHVariantSpec): VcfDataSource { xhr.responseType = 'json'; xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.addEventListener('load', function(e) { + xhr.addEventListener('load', function(e: any) { var response = this.response; if (this.status >= 400) { notifyFailure(this.status + ' ' + this.statusText + ' ' + JSON.stringify(response)); @@ -96,7 +96,7 @@ function create(spec: GA4GHVariantSpec): VcfDataSource { } } }); - xhr.addEventListener('error', function(e) { + xhr.addEventListener('error', function(e: any) { notifyFailure('Request failed with status: ' + this.status); }); @@ -128,7 +128,7 @@ function create(spec: GA4GHVariantSpec): VcfDataSource { on: () => {}, once: () => {}, off: () => {}, - trigger: () => {} + trigger: (status: string, param: any) => {} }; _.extend(o, Events); // Make this an event emitter return o; diff --git a/src/main/sources/TwoBitDataSource.js b/src/main/sources/TwoBitDataSource.js index 361ee7d5..c4d993da 100644 --- a/src/main/sources/TwoBitDataSource.js +++ b/src/main/sources/TwoBitDataSource.js @@ -19,6 +19,7 @@ import Q from 'q'; import _ from 'underscore'; import {Events} from 'backbone'; +import type {GenomeRange} from '../types'; import ContigInterval from '../ContigInterval'; import TwoBit from '../data/TwoBit'; import RemoteFile from '../RemoteFile'; @@ -58,7 +59,7 @@ var createFromTwoBitFile = function(remoteSource: TwoBit): TwoBitSource { // Ranges for which we have complete information -- no need to hit network. var coveredRanges = ([]: ContigInterval[]); - function fetch(range: ContigInterval) { + function fetch(range: ContigInterval) { var span = range.length(); if (span > MAX_BASE_PAIRS_TO_FETCH) { //inform that we won't fetch the data @@ -153,7 +154,7 @@ var createFromTwoBitFile = function(remoteSource: TwoBit): TwoBitSource { on: () => {}, once: () => {}, off: () => {}, - trigger: () => {} + trigger: (status: string, param: any) => {} }; _.extend(o, Events); // Make this an event emitter diff --git a/src/main/sources/VcfDataSource.js b/src/main/sources/VcfDataSource.js index 2adcbcfd..4c9636e9 100644 --- a/src/main/sources/VcfDataSource.js +++ b/src/main/sources/VcfDataSource.js @@ -9,7 +9,7 @@ import Events from 'backbone'; import _ from 'underscore'; import Q from 'q'; - +import type {GenomeRange} from '../types'; import ContigInterval from '../ContigInterval'; import RemoteFile from '../RemoteFile'; import LocalStringFile from '../LocalStringFile'; @@ -81,7 +81,7 @@ function createFromVcfFile(remoteSource: VcfFile): VcfDataSource { // These are here to make Flow happy. on: () => {}, off: () => {}, - trigger: () => {} + trigger: (status: string, param: any) => {} }; _.extend(o, Events); // Make this an event emitter diff --git a/src/main/types.js b/src/main/types.js index d55b5f50..68d5f673 100644 --- a/src/main/types.js +++ b/src/main/types.js @@ -11,7 +11,7 @@ // Public API -import type React from 'react'; +import React from 'react'; export const AllelFrequencyStrategy = { Minor : {name: "Minor"}, @@ -26,7 +26,7 @@ export type State = { }; export type VizWithOptions = { - component: ReactClass; + component: Class>; options: ?Object; } @@ -45,14 +45,12 @@ export type VisualizedTrack = { track: Track; // for css class and options } -/* -TODO(danvk): kill types/types.js and use this export type GenomeRange = { contig: string; start: number; // inclusive stop: number; // inclusive } -*/ + export type PartialGenomeRange = { contig?: string; start?: number; diff --git a/src/main/viz/AbstractCache.js b/src/main/viz/AbstractCache.js index ba46e244..38d0d9fa 100644 --- a/src/main/viz/AbstractCache.js +++ b/src/main/viz/AbstractCache.js @@ -25,9 +25,9 @@ export type VisualGroup = { items: T[]; }; -class AbstractCache { +class AbstractCache { // maps groupKey to VisualGroup - groups: {[key: string]: VisualGroup}; + groups: {[key: string]: VisualGroup}; refToPileup: {[key: string]: Array}; referenceSource: TwoBitSource; @@ -58,7 +58,7 @@ class AbstractCache { } // Find groups overlapping the range. This is 'chr'-agnostic. - getGroupsOverlapping(range: ContigInterval): VisualGroup[] { + getGroupsOverlapping(range: ContigInterval): VisualGroup[] { // TODO: speed this up using an interval tree return _.filter(this.groups, group => group.span.intersects(range)); } diff --git a/src/main/viz/CoverageCache.js b/src/main/viz/CoverageCache.js index 3c66d182..d760bb4b 100644 --- a/src/main/viz/CoverageCache.js +++ b/src/main/viz/CoverageCache.js @@ -7,11 +7,10 @@ */ 'use strict'; -import type {Strand, Alignment, AlignmentDataSource} from '../Alignment'; +import type {Alignment} from '../Alignment'; import type {TwoBitSource} from '../sources/TwoBitDataSource'; -import type {BasePair, OpInfo} from './pileuputils'; +import type {OpInfo} from './pileuputils'; import type ContigInterval from '../ContigInterval'; -import type Interval from '../Interval'; import {getOpInfo} from './pileuputils'; import utils from '../utils'; diff --git a/src/main/viz/CoverageTrack.js b/src/main/viz/CoverageTrack.js index ec0af269..f2002636 100644 --- a/src/main/viz/CoverageTrack.js +++ b/src/main/viz/CoverageTrack.js @@ -4,12 +4,12 @@ */ 'use strict'; -import type {Alignment, AlignmentDataSource} from '../Alignment'; -import type Interval from '../Interval'; -import type {TwoBitSource} from '../sources/TwoBitDataSource'; +import type {AlignmentDataSource} from '../Alignment'; import type {DataCanvasRenderingContext2D} from 'data-canvas'; import type {BinSummary} from './CoverageCache'; import type {Scale} from './d3utils'; +import type {State} from '../types'; +import type {VizProps} from '../VisualizationWrapper'; import React from 'react'; import scale from '../scale'; @@ -168,25 +168,14 @@ function renderBars(ctx: DataCanvasRenderingContext2D, }); } -type Props = { - width: number; - height: number; - range: GenomeRange; - source: AlignmentDataSource; - referenceSource: TwoBitSource; - options: { - vafColorThreshold: number - } -}; - -class CoverageTrack extends React.Component { - props: Props; - state: void; +class CoverageTrack extends React.Component, State> { + props: VizProps; + state: State; // no state, used to make flow happy cache: CoverageCache; tiles: CoverageTiledCanvas; static defaultOptions: Object; - - constructor(props: Props) { + + constructor(props: VizProps) { super(props); } @@ -333,5 +322,4 @@ CoverageTrack.defaultOptions = { vafColorThreshold: 0.2 }; - module.exports = CoverageTrack; diff --git a/src/main/viz/FeatureTrack.js b/src/main/viz/FeatureTrack.js index 40c0bdca..905a31b7 100644 --- a/src/main/viz/FeatureTrack.js +++ b/src/main/viz/FeatureTrack.js @@ -5,7 +5,6 @@ 'use strict'; import type {FeatureDataSource} from '../sources/BigBedDataSource'; -import type Feature from '../data/feature'; import GenericFeature from '../data/genericFeature'; import {GenericFeatureCache} from './GenericFeatureCache'; import type {VisualGroup} from './AbstractCache'; @@ -25,7 +24,7 @@ import canvasUtils from './canvas-utils'; import TiledCanvas from './TiledCanvas'; import dataCanvas from 'data-canvas'; import style from '../style'; -import type {State, NetworkStatus} from '../types'; +import type {State} from '../types'; import {yForRow} from './pileuputils'; @@ -71,7 +70,7 @@ class FeatureTiledCanvas extends TiledCanvas { function renderFeatures(ctx: DataCanvasRenderingContext2D, scale: (num: number) => number, range: ContigInterval, - vFeatures: VisualGroup[]) { + vFeatures: VisualGroup[]) { ctx.font = `${style.GENE_FONT_SIZE}px ${style.GENE_FONT}`; ctx.textAlign = 'center'; @@ -94,13 +93,13 @@ function renderFeatures(ctx: DataCanvasRenderingContext2D, }); } -class FeatureTrack extends React.Component { - props: VizProps & { source: FeatureDataSource }; +class FeatureTrack extends React.Component, State> { + props: VizProps; state: State; tiles: FeatureTiledCanvas; cache: GenericFeatureCache; - constructor(props: VizProps) { + constructor(props: VizProps) { super(props); this.state = { networkStatus: null diff --git a/src/main/viz/GeneTrack.js b/src/main/viz/GeneTrack.js index 6ee6b740..361f75b7 100644 --- a/src/main/viz/GeneTrack.js +++ b/src/main/viz/GeneTrack.js @@ -8,6 +8,10 @@ import type {Strand} from '../Alignment'; import type {Gene, BigBedSource} from '../sources/BigBedDataSource'; import type {VizProps} from '../VisualizationWrapper'; import type {Scale} from './d3utils'; +import type {State} from '../types'; + +import GenericFeature from '../data/genericFeature'; +import {GenericFeatureCache} from './GenericFeatureCache'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -68,15 +72,13 @@ function drawGeneName(ctx: CanvasRenderingContext2D, } } -class GeneTrack extends React.Component { - props: VizProps & { source: BigBedSource }; - state: {genes: Gene[]}; +class GeneTrack extends React.Component, State> { + props: VizProps; + state: State; + cache: GenericFeatureCache; - constructor(props: VizProps) { + constructor(props: VizProps) { super(props); - this.state = { - genes: [] - }; } render(): any { @@ -84,11 +86,22 @@ class GeneTrack extends React.Component { } componentDidMount() { - // Visualize new reference data as it comes in from the network. + this.cache = new GenericFeatureCache(this.props.referenceSource); + // Visualize new reference data as it comes in from the network. this.props.source.on('newdata', (range) => { - this.setState({ - genes: this.props.source.getFeaturesInRange(range) - }); + // add genes to generic cache + var genes = this.props.source.getFeaturesInRange(range); + genes.forEach(f => this.cache.addFeature(new GenericFeature(f.id, f.position, f))); + this.updateVisualization(); + }); + this.props.referenceSource.on('newdata', range => { + this.updateVisualization(); + }); + this.props.source.on('networkprogress', e => { + this.setState({networkStatus: e}); + }); + this.props.source.on('networkdone', e => { + this.setState({networkStatus: null}); }); this.updateVisualization(); @@ -99,9 +112,9 @@ class GeneTrack extends React.Component { } componentDidUpdate(prevProps: any, prevState: any) { - if (!shallowEquals(prevProps, this.props) || - !shallowEquals(prevState, this.state)) { - this.updateVisualization(); + if (!shallowEquals(this.props, prevProps) || + !shallowEquals(this.state, prevState)) { + this.updateVisualization(); } } @@ -122,50 +135,59 @@ class GeneTrack extends React.Component { .range([0, width]) .clamp(true); - d3utils.sizeCanvas(canvas, width, height); - - var ctx = dataCanvas.getDataContext(canvasUtils.getContext(canvas)); - ctx.reset(); - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - - var geneLineY = Math.round(height / 4); - var textIntervals = []; // x-intervals with rendered gene names, to avoid over-drawing. - // TODO: don't pull in genes via state. - ctx.font = `${style.GENE_FONT_SIZE}px ${style.GENE_FONT}`; - ctx.textAlign = 'center'; - this.state.genes.forEach(gene => { - if (!gene.position.intersects(range)) return; - ctx.pushObject(gene); - ctx.lineWidth = 1; - ctx.strokeStyle = style.GENE_COLOR; - ctx.fillStyle = style.GENE_COLOR; - - canvasUtils.drawLine(ctx, clampedScale(1 + gene.position.start()), geneLineY + 0.5, - clampedScale(1 + gene.position.stop()), geneLineY + 0.5); - - // TODO: only compute all these intervals when data becomes available. - var exons = bedtools.splitCodingExons(gene.exons, gene.codingRegion); - exons.forEach(exon => { - ctx.fillRect(sc(1 + exon.start), - geneLineY - 3 * (exon.isCoding ? 2 : 1), - sc(exon.stop + 2) - sc(1 + exon.start), - 6 * (exon.isCoding ? 2 : 1)); - }); - var introns = gene.position.interval.complementIntervals(gene.exons); - introns.forEach(range => { - drawArrow(ctx, clampedScale, range, geneLineY + 0.5, gene.strand); - }); - ctx.strokeStyle = style.GENE_COMPLEMENT_COLOR; - ctx.lineWidth = 2; - gene.exons.forEach(range => { - drawArrow(ctx, clampedScale, range, geneLineY + 0.5, gene.strand); - }); - drawGeneName(ctx, clampedScale, geneLineY, gene, textIntervals); - - ctx.popObject(); - }); + if (canvas && canvas instanceof Element) { // check for getContext + if (canvas instanceof HTMLCanvasElement) { // check for sizeCanvas + d3utils.sizeCanvas(canvas, width, height); + } + var ctx = dataCanvas.getDataContext(canvasUtils.getContext(canvas)); + + ctx.reset(); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + var geneLineY = Math.round(height / 4); + var textIntervals = []; // x-intervals with rendered gene names, to avoid over-drawing. + ctx.font = `${style.GENE_FONT_SIZE}px ${style.GENE_FONT}`; + ctx.textAlign = 'center'; + var vFeatures = _.flatten(_.map(this.cache.getGroupsOverlapping(range), + g => g.items)); + + vFeatures.forEach(vFeature => { + var gene = vFeature.gFeature; + if (!gene.position.intersects(range)) return; + ctx.pushObject(gene); + ctx.lineWidth = 1; + ctx.strokeStyle = style.GENE_COLOR; + ctx.fillStyle = style.GENE_COLOR; + + canvasUtils.drawLine(ctx, clampedScale(1 + gene.position.start()), geneLineY + 0.5, + clampedScale(1 + gene.position.stop()), geneLineY + 0.5); + + // TODO: only compute all these intervals when data becomes available. + var exons = bedtools.splitCodingExons(gene.exons, gene.codingRegion); + exons.forEach(exon => { + ctx.fillRect(sc(1 + exon.start), + geneLineY - 3 * (exon.isCoding ? 2 : 1), + sc(exon.stop + 2) - sc(1 + exon.start), + 6 * (exon.isCoding ? 2 : 1)); + }); + + var introns = gene.position.interval.complementIntervals(gene.exons); + introns.forEach(range => { + drawArrow(ctx, clampedScale, range, geneLineY + 0.5, gene.strand); + }); + ctx.strokeStyle = style.GENE_COMPLEMENT_COLOR; + ctx.lineWidth = 2; + gene.exons.forEach(range => { + drawArrow(ctx, clampedScale, range, geneLineY + 0.5, gene.strand); + }); + + drawGeneName(ctx, clampedScale, geneLineY, gene, textIntervals); + + ctx.popObject(); + }); + } // end typecheck for canvasß } } diff --git a/src/main/viz/GenericFeatureCache.js b/src/main/viz/GenericFeatureCache.js index 275a2db1..074d1e6e 100644 --- a/src/main/viz/GenericFeatureCache.js +++ b/src/main/viz/GenericFeatureCache.js @@ -19,9 +19,9 @@ import AbstractCache from './AbstractCache'; import type {VisualGroup} from './AbstractCache'; // This class provides data management for the visualization -class GenericFeatureCache extends AbstractCache { +class GenericFeatureCache extends AbstractCache { // maps groupKey to VisualGroup - groups: {[key: string]: VisualGroup}; + groups: {[key: string]: VisualGroup}; refToPileup: {[key: string]: Array}; referenceSource: TwoBitSource; diff --git a/src/main/viz/GenomeTrack.js b/src/main/viz/GenomeTrack.js index 34d2c1f5..c1ef4649 100644 --- a/src/main/viz/GenomeTrack.js +++ b/src/main/viz/GenomeTrack.js @@ -12,6 +12,7 @@ import type {Scale} from './d3utils'; import React from 'react'; import ReactDOM from 'react-dom'; import shallowEquals from 'shallow-equals'; +import type {State} from '../types'; import canvasUtils from './canvas-utils'; import ContigInterval from '../ContigInterval'; @@ -110,10 +111,9 @@ class GenomeTiledCanvas extends TiledCanvas { } } - -class GenomeTrack extends React.Component { - props: VizProps & {source: TwoBitSource}; - state: void; // no state +class GenomeTrack extends React.Component, State> { + props: VizProps; + state: State; // no state, used to make flow happy tiles: GenomeTiledCanvas; render(): any { @@ -153,12 +153,17 @@ class GenomeTrack extends React.Component { // Hold off until height & width are known. if (width === 0) return; - d3utils.sizeCanvas(canvas, width, height); - var ctx = dataCanvas.getDataContext(canvasUtils.getContext(canvas)); - ctx.reset(); - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - this.tiles.renderToScreen(ctx, ContigInterval.fromGenomeRange(range), this.getScale()); + if (canvas && canvas instanceof Element) { // check for getContext + if (canvas instanceof HTMLCanvasElement) { // check for sizeCanvas + d3utils.sizeCanvas(canvas, width, height); + } + var ctx = dataCanvas.getDataContext(canvasUtils.getContext(canvas)); + + ctx.reset(); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + this.tiles.renderToScreen(ctx, ContigInterval.fromGenomeRange(range), this.getScale()); + } // end typecheck for canvas } } diff --git a/src/main/viz/LocationTrack.js b/src/main/viz/LocationTrack.js index 4953ad8b..4d55050e 100644 --- a/src/main/viz/LocationTrack.js +++ b/src/main/viz/LocationTrack.js @@ -6,7 +6,7 @@ import type {VizProps} from '../VisualizationWrapper'; import type {Scale} from './d3utils'; - +import type {State} from '../types'; import React from 'react'; import ReactDOM from 'react-dom'; import EmptySource from '../sources/EmptySource'; @@ -15,12 +15,12 @@ import dataCanvas from 'data-canvas'; import style from '../style'; import d3utils from './d3utils'; -class LocationTrack extends React.Component { - props: VizProps; - state: void; // no state +class LocationTrack extends React.Component, State> { + props: VizProps; + state: State; // state not used, here to make flow happy static defaultSource: Object; - constructor(props: Object) { + constructor(props: VizProps) { super(props); } @@ -45,37 +45,40 @@ class LocationTrack extends React.Component { {range, width, height} = this.props, scale = this.getScale(); - d3utils.sizeCanvas(canvas, width, height); - - var ctx = dataCanvas.getDataContext(canvasUtils.getContext(canvas)); - ctx.save(); - ctx.reset(); - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + if (canvas && canvas instanceof Element) { // check for getContext + if (canvas instanceof HTMLCanvasElement) { // check for sizeCanvas + d3utils.sizeCanvas(canvas, width, height); + } + var ctx = dataCanvas.getDataContext(canvasUtils.getContext(canvas)); + ctx.save(); + ctx.reset(); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - var midPoint = Math.floor((range.stop + range.start) / 2), - rightLineX = Math.round(scale(midPoint + 1)), - leftLineX = Math.round(scale(midPoint)); + var midPoint = Math.floor((range.stop + range.start) / 2), + rightLineX = Math.round(scale(midPoint + 1)), + leftLineX = Math.round(scale(midPoint)); - // Left line - canvasUtils.drawLine(ctx, leftLineX - 0.5, 0, leftLineX - 0.5, height); + // Left line + canvasUtils.drawLine(ctx, leftLineX - 0.5, 0, leftLineX - 0.5, height); - // Right line - canvasUtils.drawLine(ctx, rightLineX - 0.5, 0, rightLineX - 0.5, height); + // Right line + canvasUtils.drawLine(ctx, rightLineX - 0.5, 0, rightLineX - 0.5, height); - // Mid label - var midY = height / 2; + // Mid label + var midY = height / 2; - ctx.fillStyle = style.LOC_FONT_COLOR; - ctx.font = style.LOC_FONT_STYLE; - ctx.fillText(midPoint.toLocaleString() + ' bp', - rightLineX + style.LOC_TICK_LENGTH + style.LOC_TEXT_PADDING, - midY + style.LOC_TEXT_Y_OFFSET); + ctx.fillStyle = style.LOC_FONT_COLOR; + ctx.font = style.LOC_FONT_STYLE; + ctx.fillText(midPoint.toLocaleString() + ' bp', + rightLineX + style.LOC_TICK_LENGTH + style.LOC_TEXT_PADDING, + midY + style.LOC_TEXT_Y_OFFSET); - // Connect label with the right line - canvasUtils.drawLine(ctx, rightLineX - 0.5, midY - 0.5, rightLineX + style.LOC_TICK_LENGTH - 0.5, midY - 0.5); + // Connect label with the right line + canvasUtils.drawLine(ctx, rightLineX - 0.5, midY - 0.5, rightLineX + style.LOC_TICK_LENGTH - 0.5, midY - 0.5); - // clean up - ctx.restore(); + // clean up + ctx.restore(); + } // end typecheck for canvas } } diff --git a/src/main/viz/PileupCache.js b/src/main/viz/PileupCache.js index 069d7c8d..c40ffc6b 100644 --- a/src/main/viz/PileupCache.js +++ b/src/main/viz/PileupCache.js @@ -7,7 +7,7 @@ */ 'use strict'; -import type {Strand, Alignment, AlignmentDataSource} from '../Alignment'; +import type {Strand, Alignment} from '../Alignment'; import type {TwoBitSource} from '../sources/TwoBitDataSource'; import type {BasePair} from './pileuputils'; import AbstractCache from './AbstractCache'; @@ -42,9 +42,9 @@ export type InsertStats = { // This class provides data management for the visualization, grouping paired // reads and managing the pileup. -class PileupCache extends AbstractCache { +class PileupCache extends AbstractCache { // maps groupKey to VisualGroup - groups: {[key: string]: VisualGroup}; + groups: {[key: string]: VisualGroup}; refToPileup: {[key: string]: Array}; referenceSource: TwoBitSource; viewAsPairs: boolean; diff --git a/src/main/viz/PileupTrack.js b/src/main/viz/PileupTrack.js index 08b47c24..87179447 100644 --- a/src/main/viz/PileupTrack.js +++ b/src/main/viz/PileupTrack.js @@ -4,13 +4,11 @@ */ 'use strict'; -import type {Strand, Alignment, AlignmentDataSource} from '../Alignment'; -import type {TwoBitSource} from '../sources/TwoBitDataSource'; +import type {AlignmentDataSource} from '../Alignment'; import type {BasePair} from './pileuputils'; import type {VisualAlignment, InsertStats} from './PileupCache'; import type {VisualGroup} from './AbstractCache'; import type {DataCanvasRenderingContext2D} from 'data-canvas'; -import type Interval from '../Interval'; import type {VizProps} from '../VisualizationWrapper'; import type {Scale} from './d3utils'; import type {State, NetworkStatus} from '../types'; @@ -82,7 +80,7 @@ function renderPileup(ctx: DataCanvasRenderingContext2D, range: ContigInterval, insertStats: ?InsertStats, colorByStrand: boolean, - vGroups: VisualGroup[]) { + vGroups: VisualGroup[]) { // Should mismatched base pairs be shown as blocks of color or as letters? var pxPerLetter = scale(1) - scale(0), mode = DisplayMode.getDisplayMode(pxPerLetter), @@ -164,7 +162,7 @@ function renderPileup(ctx: DataCanvasRenderingContext2D, ctx.popObject(); } - function drawGroup(vGroup: VisualGroup) { + function drawGroup(vGroup: VisualGroup) { ctx.save(); if (insertStats && vGroup.insert) { var len = vGroup.span.length(); @@ -229,9 +227,8 @@ function opacityForQuality(quality: number): number { return Math.min(1.0, alpha); } - -class PileupTrack extends React.Component { - props: VizProps & { source: AlignmentDataSource }; +class PileupTrack extends React.Component, State> { + props: VizProps; state: State; cache: PileupCache; tiles: PileupTiledCanvas; @@ -239,7 +236,7 @@ class PileupTrack extends React.Component { static getOptionsMenu: (options: Object) => any; static handleSelectOption: (key: string, oldOptions: Object) => Object; - constructor(props: VizProps) { + constructor(props: VizProps) { super(props); this.state = { networkStatus: null diff --git a/src/main/viz/ScaleTrack.js b/src/main/viz/ScaleTrack.js index f46edfa0..389f21c4 100644 --- a/src/main/viz/ScaleTrack.js +++ b/src/main/viz/ScaleTrack.js @@ -12,7 +12,7 @@ import type {VizProps} from '../VisualizationWrapper'; import type {Scale} from './d3utils'; - +import type {State} from '../types'; import React from 'react'; import ReactDOM from 'react-dom'; import EmptySource from '../sources/EmptySource'; @@ -21,12 +21,12 @@ import dataCanvas from 'data-canvas'; import style from '../style'; import d3utils from './d3utils'; -class ScaleTrack extends React.Component { - props: VizProps; - state: void; // no state +class ScaleTrack extends React.Component, State> { + props: VizProps; + state: State; // no state, used to make flow happy static defaultSource: Object; - constructor(props: Object) { + constructor(props: VizProps) { super(props); } diff --git a/src/main/viz/TiledCanvas.js b/src/main/viz/TiledCanvas.js index 8e067805..158f708d 100644 --- a/src/main/viz/TiledCanvas.js +++ b/src/main/viz/TiledCanvas.js @@ -113,7 +113,7 @@ class TiledCanvas { this.tileCache = []; } - invalidateRange(range: ContigInterval) { + invalidateRange(range: ContigInterval) { this.tileCache = this.tileCache.filter(tile => !tile.range.intersects(range)); } diff --git a/src/main/viz/VariantTrack.js b/src/main/viz/VariantTrack.js index 5350bad2..f2c0c7e2 100644 --- a/src/main/viz/VariantTrack.js +++ b/src/main/viz/VariantTrack.js @@ -7,11 +7,10 @@ import {AllelFrequencyStrategy} from '../types'; import type {VcfDataSource} from '../sources/VcfDataSource'; -import type {Variant} from '../data/vcf'; import type {DataCanvasRenderingContext2D} from 'data-canvas'; import type {VizProps} from '../VisualizationWrapper'; import type {Scale} from './d3utils'; - +import type {State} from '../types'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -22,12 +21,12 @@ import canvasUtils from './canvas-utils'; import dataCanvas from 'data-canvas'; import style from '../style'; -class VariantTrack extends React.Component { - props: VizProps & {source: VcfDataSource}; +class VariantTrack extends React.Component, State> { + props: VizProps; - state: void; + state: State; - constructor(props: Object) { + constructor(props: VizProps) { super(props); } @@ -60,11 +59,17 @@ class VariantTrack extends React.Component { // Hold off until height & width are known. if (width === 0) return; - - d3utils.sizeCanvas(canvas, width, height); - var ctx = canvasUtils.getContext(canvas); - var dtx = dataCanvas.getDataContext(ctx); - this.renderScene(dtx); + + if (canvas && canvas instanceof Element) { // check for getContext + if (canvas instanceof HTMLCanvasElement) { // check for sizeCanvas + d3utils.sizeCanvas(canvas, width, height); + } + var ctx = canvasUtils.getContext(canvas); + var dtx = dataCanvas.getDataContext(ctx); + this.renderScene(dtx); + } else { + throw new TypeError("canvas is not an Element"); + } } renderScene(ctx: DataCanvasRenderingContext2D) { @@ -101,8 +106,7 @@ class VariantTrack extends React.Component { var height = style.VARIANT_HEIGHT*variantHeightRatio; var variantY = y - 0.5 + style.VARIANT_HEIGHT - height; var variantX = Math.round(scale(variant.position)) - 0.5; - var width = Math.round(scale(variant.position + 1)) - 0.5 - variantX; - + var width = Math.max(1, Math.round(scale(variant.position + 1) - scale(variant.position))); ctx.pushObject(variant); ctx.fillRect(variantX, variantY, width, height); @@ -117,27 +121,34 @@ class VariantTrack extends React.Component { var ev = reactEvent.nativeEvent, x = ev.offsetX, y = ev.offsetY, - canvas = ReactDOM.findDOMNode(this), - ctx = canvasUtils.getContext(canvas), - trackingCtx = new dataCanvas.ClickTrackingContext(ctx, x, y); - this.renderScene(trackingCtx); - - var variants = trackingCtx.hit; - if (variants && variants.length>0) { - var data = []; - for (var i=0;i0) { + var data = []; + for (var i=0;i number; */ function getTrackScale(range: Range, width: number): any { if (!range) return scale.linear(); - var offsetPx = range.offsetPx || 0; return scale.linear() .domain([range.start, range.stop + 1]) // 1 bp wide - .range([-offsetPx, width - offsetPx]); + .range([0, width]); } var formatPrefixes = ["","k","M","G","T","P","E","Z","Y"]; diff --git a/src/main/viz/pileuputils.js b/src/main/viz/pileuputils.js index b5b8dd32..2f887769 100644 --- a/src/main/viz/pileuputils.js +++ b/src/main/viz/pileuputils.js @@ -1,7 +1,6 @@ /** @flow */ 'use strict'; -import type SamRead from '../data/SamRead'; import type {Alignment, CigarSymbol} from '../Alignment'; import type Interval from '../Interval'; import style from '../style'; @@ -160,7 +159,7 @@ function getOpInfo(read: Alignment, referenceSource: Object): OpInfo { refPos = start, arrowIndex = getArrowIndex(read); - var result = []; + var result: Op[] = []; var mismatches = ([]: BasePair[]); for (var i = 0; i < ops.length; i++) { var op = ops[i]; @@ -178,7 +177,7 @@ function getOpInfo(read: Alignment, referenceSource: Object): OpInfo { op: op.op, length: op.length, pos: refPos, - arrow: null + arrow: null // direction will be set below }); // These are the cigar operations which advance position in the reference diff --git a/src/test/.eslintrc.json b/src/test/.eslintrc.json new file mode 100644 index 00000000..d17a3b49 --- /dev/null +++ b/src/test/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/src/test/ContigInterval-test.js b/src/test/ContigInterval-test.js index b82b2ee1..2b558d48 100644 --- a/src/test/ContigInterval-test.js +++ b/src/test/ContigInterval-test.js @@ -5,33 +5,37 @@ import {expect} from 'chai'; import ContigInterval from '../main/ContigInterval'; -describe('ContigInterval', function() { - it('should have basic accessors', function() { +describe('ContigInterval', function(done) { + + it('should have basic accessors', function(done) { var tp53 = new ContigInterval(10, 7512444, 7531643); expect(tp53.toString()).to.equal('10:7512444-7531643'); expect(tp53.contig).to.equal(10); expect(tp53.start()).to.equal(7512444); expect(tp53.stop()).to.equal(7531643); expect(tp53.length()).to.equal(19200); + done(); }); - it('should determine intersections', function() { + it('should determine intersections', function(done) { var tp53 = new ContigInterval(10, 7512444, 7531643); var other = new ContigInterval(10, 7512444, 7531642); expect(tp53.intersects(other)).to.be.true; + done(); }); - it('should determine containment', function() { + it('should determine containment', function(done) { var ci = new ContigInterval('20', 1000, 2000); expect(ci.containsLocus('20', 999)).to.be.false; expect(ci.containsLocus('20', 1000)).to.be.true; expect(ci.containsLocus('20', 2000)).to.be.true; expect(ci.containsLocus('20', 2001)).to.be.false; expect(ci.containsLocus('21', 1500)).to.be.false; + done(); }); - it('should coalesce lists of intervals', function() { + it('should coalesce lists of intervals', function(done) { var ci = (a, b, c) => new ContigInterval(a, b, c); var coalesceToString = @@ -83,9 +87,10 @@ describe('ContigInterval', function() { expect(ci1.toString()).to.equal('0:20-30'); expect(ci2.toString()).to.equal('0:5-18'); expect(ci3.toString()).to.equal('0:0-10'); + done(); }); - it('should determine coverage', function() { + it('should determine coverage', function(done) { var iv = new ContigInterval(1, 10, 20); expect(iv.isCoveredBy([ new ContigInterval(1, 0, 10), @@ -133,5 +138,6 @@ describe('ContigInterval', function() { new ContigInterval(1, 5, 15), new ContigInterval(1, 0, 10) ]))).to.be.false; + done(); }); }); diff --git a/src/test/FakeAlignment.js b/src/test/FakeAlignment.js index de67c4a4..d31d842a 100644 --- a/src/test/FakeAlignment.js +++ b/src/test/FakeAlignment.js @@ -6,6 +6,8 @@ import type {Alignment, CigarOp, MateProperties, Strand} from '../main/Alignment'; import type ContigInterval from '../main/ContigInterval'; +import Q from 'q'; +import type {GenomeRange} from '../main/types'; var numAlignments = 1; class FakeAlignment /* implements Alignment */ { @@ -64,7 +66,7 @@ var fakeSource = { getRange: function(): any { return {}; }, getRangeAsString: function(): string { return ''; }, contigList: function(): string[] { return []; }, - normalizeRange: function() { }, + normalizeRange: function(range: GenomeRange): Q.Promise { return Q.when(range); }, on: dieFn, off: dieFn, once: dieFn, diff --git a/src/test/GA4GHAlignment-test.js b/src/test/GA4GHAlignment-test.js index 092d9b45..387528a4 100644 --- a/src/test/GA4GHAlignment-test.js +++ b/src/test/GA4GHAlignment-test.js @@ -10,17 +10,18 @@ import Bam from '../main/data/bam'; describe('GA4GHAlignment', function() { var sampleAlignments = []; - before(function() { + before(function(): any { return new RemoteFile('/test-data/alignments.ga4gh.1.10000-11000.json').getAllString().then(data => { sampleAlignments = JSON.parse(data).alignments; }); }); - it('should read the sample alignments', function() { + it('should read the sample alignments', function(done) { expect(sampleAlignments).to.have.length(100); + done(); }); - it('should provide basic accessors', function() { + it('should provide basic accessors', function(done) { var a = new GA4GHAlignment(sampleAlignments[0]); expect(a.name).to.equal('ERR181329.21587964'); expect(a.getSequence()).to.equal('ATAACCCTAACCATAACCCTAACCCTAACCCTAACCCTAACCCTAACCCTAACCCTAACCCTAACCCTAACCCTAACCCTAACCCTAACCCTAACCCTAA'); @@ -35,9 +36,10 @@ describe('GA4GHAlignment', function() { pos: 10007, strand: '-' }); + done(); }); - it('should match SamRead', function() { + it('should match SamRead', function(): any { var bam = new Bam(new RemoteFile('/test-data/chr17.1-250.bam')); var json = new RemoteFile('/test-data/alignments.ga4gh.chr17.1-250.json'); diff --git a/src/test/Interval-test.js b/src/test/Interval-test.js index 6acf2bf4..351db797 100644 --- a/src/test/Interval-test.js +++ b/src/test/Interval-test.js @@ -6,48 +6,53 @@ import {expect} from 'chai'; import Interval from '../main/Interval'; describe('Interval', function() { - it('should have start/stop/length', function() { + it('should have start/stop/length', function(done) { var x = new Interval(10, 20); expect(x.start).to.equal(10); expect(x.stop).to.equal(20); expect(x.length()).to.equal(11); expect(x.toString()).to.equal('[10, 20]'); + done(); }); - it('should determine containment', function() { + it('should determine containment', function(done) { var x = new Interval(-10, 10); expect(x.contains(0)).to.be.true; expect(x.contains(-10)).to.be.true; expect(x.contains(+10)).to.be.true; expect(x.contains(+11)).to.be.false; expect(x.contains(-11)).to.be.false; + done(); }); - it('should work with empty intervals', function() { + it('should work with empty intervals', function(done) { var empty = new Interval(5, 0), other = new Interval(-10, 10); expect(empty.contains(0)).to.be.false; expect(empty.length()).to.equal(0); expect(empty.intersect(other).length()).to.equal(0); + done(); }); - it('should determine intersections', function() { + it('should determine intersections', function(done) { var tp53 = new Interval(7512444, 7531643); var other = new Interval(7512444, 7531642); expect(tp53.intersects(other)).to.be.true; + done(); }); - it('should clone', function() { + it('should clone', function(done) { var x = new Interval(0, 5), y = x.clone(); y.start = 1; expect(x.start).to.equal(0); expect(y.start).to.equal(1); + done(); }); - it('should intersect many intervals', function() { + it('should intersect many intervals', function(done) { var ivs = [ new Interval(0, 10), new Interval(5, 15), @@ -61,9 +66,10 @@ describe('Interval', function() { expect(intAll([ivs[0] ]).toString()).to.equal('[0, 10]'); expect(() => intAll([])).to.throw(/intersect zero intervals/); + done(); }); - it('should construct bounding intervals', function() { + it('should construct bounding intervals', function(done) { var ivs = [ new Interval(0, 10), new Interval(5, 15), @@ -77,9 +83,10 @@ describe('Interval', function() { expect(bound([ivs[0] ]).toString()).to.equal('[0, 10]'); expect(() => bound([])).to.throw(/bound zero intervals/); + done(); }); - it('should determine coverage', function() { + it('should determine coverage', function(done) { var iv = new Interval(10, 20); expect(iv.isCoveredBy([ new Interval(0, 10), @@ -120,9 +127,10 @@ describe('Interval', function() { new Interval(5, 15), new Interval(0, 10) ])).to.throw(/sorted ranges/); + done(); }); - it('should subtract intervals', function() { + it('should subtract intervals', function(done) { // 0123456789 // a ---------- // b --- @@ -151,9 +159,10 @@ describe('Interval', function() { expect(d.subtract(b).map(x => x.toString())).to.deep.equal([d.toString()]); expect(d.subtract(c).map(x => x.toString())).to.deep.equal(['[3, 6]']); expect(d.subtract(d).map(x => x.toString())).to.deep.equal([]); + done(); }); - it('should compute complements', function() { + it('should compute complements', function(done) { var iv = new Interval(0, 99); var exons = [ new Interval(10, 19), @@ -167,9 +176,10 @@ describe('Interval', function() { '[20, 29]', '[50, 79]' ]); + done(); }); - it('should round interval', function() { + it('should round interval', function(done) { var interval = new Interval(1, 20); var rounded = interval.round(40, true); expect(rounded.start).to.equal(0); @@ -177,5 +187,6 @@ describe('Interval', function() { rounded = interval.round(40, false); expect(rounded.start).to.equal(1); + done(); }); }); diff --git a/src/test/LocalStringFile-test.js b/src/test/LocalStringFile-test.js index ca8d519f..cf987dae 100644 --- a/src/test/LocalStringFile-test.js +++ b/src/test/LocalStringFile-test.js @@ -1,7 +1,7 @@ /* @flow */ 'use strict'; -import {expect} from 'chai'; +import {expect, fail} from 'chai'; import LocalStringFile from '../main/LocalStringFile'; import jBinary from 'jbinary'; @@ -11,44 +11,56 @@ describe('LocalStringFile', () => { return new jBinary(buf).read('string'); } - it('should fetch a subset of a file', function() { + it('should fetch a subset of a file', function(): any { var f = new LocalStringFile('0123456789\n'); var promisedData = f.getBytes(4, 5); return promisedData.then(buf => { - expect(buf.byteLength).to.equal(5); - expect(bufferToText(buf)).to.equal('45678'); + expect(buf).to.not.be.null; + if (buf != null) { + expect(buf.byteLength).to.equal(5); + expect(bufferToText(buf)).to.equal('45678'); + } }); }); - it('should fetch subsets from cache', function() { + it('should fetch subsets from cache', function(): any { var f = new LocalStringFile('0123456789\n'); return f.getBytes(0, 10).then(buf => { - expect(buf.byteLength).to.equal(10); - expect(bufferToText(buf)).to.equal('0123456789'); + expect(buf).to.not.be.null; + if (buf != null) { + expect(buf.byteLength).to.equal(10); + expect(bufferToText(buf)).to.equal('0123456789'); + } return f.getBytes(4, 5).then(buf => { - expect(buf.byteLength).to.equal(5); - expect(bufferToText(buf)).to.equal('45678'); + expect(buf).to.not.be.null; + if (buf != null) { + expect(buf.byteLength).to.equal(5); + expect(bufferToText(buf)).to.equal('45678'); + } }); }); }); - it('should fetch entire files', function() { + it('should fetch entire files', function(): any { var f = new LocalStringFile('0123456789\n'); return f.getAll().then(buf => { - expect(buf.byteLength).to.equal(11); - expect(bufferToText(buf)).to.equal('0123456789\n'); + expect(buf).to.not.be.null; + if (buf != null) { + expect(buf.byteLength).to.equal(11); + expect(bufferToText(buf)).to.equal('0123456789\n'); + } }); }); - it('should determine file lengths', function() { + it('should determine file lengths', function(): any { var f = new LocalStringFile('0123456789\n'); return f.getSize().then(size => { expect(size).to.equal(11); }); }); - it('should get file lengths from full requests', function() { + it('should get file lengths from full requests', function(): any { var f = new LocalStringFile('0123456789\n'); return f.getAll().then(buf => { return f.getSize().then(size => { @@ -57,7 +69,7 @@ describe('LocalStringFile', () => { }); }); - it('should get file lengths from range requests', function() { + it('should get file lengths from range requests', function(): any { var f = new LocalStringFile('0123456789\n'); return f.getBytes(4, 5).then(buf => { return f.getSize().then(size => { @@ -66,45 +78,63 @@ describe('LocalStringFile', () => { }); }); - it('should cache requests for full files', function() { + it('should cache requests for full files', function(): any { var f = new LocalStringFile('0123456789\n'); - f.getAll().then(buf => { - expect(buf.byteLength).to.equal(11); - expect(bufferToText(buf)).to.equal('0123456789\n'); - return f.getAll().then(buf => { + return f.getAll().then(buf => { + expect(buf).to.not.be.null; + if (buf != null) { expect(buf.byteLength).to.equal(11); expect(bufferToText(buf)).to.equal('0123456789\n'); + } + return f.getAll().then(buf => { + expect(buf).to.not.be.null; + if (buf != null) { + expect(buf.byteLength).to.equal(11); + expect(bufferToText(buf)).to.equal('0123456789\n'); + } }); }); }); - it('should serve range requests from cache after getAll', function() { + it('should serve range requests from cache after getAll', function(): any { var f = new LocalStringFile('0123456789\n'); return f.getAll().then(buf => { - expect(buf.byteLength).to.equal(11); - expect(bufferToText(buf)).to.equal('0123456789\n'); + expect(buf).to.not.be.null; + if (buf != null) { + expect(buf.byteLength).to.equal(11); + expect(bufferToText(buf)).to.equal('0123456789\n'); + } return f.getBytes(4, 5).then(buf => { - expect(buf.byteLength).to.equal(5); - expect(bufferToText(buf)).to.equal('45678'); + expect(buf).to.not.be.null; + if (buf != null) { + expect(buf.byteLength).to.equal(5); + expect(bufferToText(buf)).to.equal('45678'); + } }); }); }); - it('should truncate requests past EOF', function() { + it('should truncate requests past EOF', function(): any { var f = new LocalStringFile('0123456789\n'); var promisedData = f.getBytes(4, 100); return promisedData.then(buf => { - expect(buf.byteLength).to.equal(7); - expect(bufferToText(buf)).to.equal('456789\n'); + expect(buf).to.not.be.null; + if (buf != null) { + expect(buf.byteLength).to.equal(7); + expect(bufferToText(buf)).to.equal('456789\n'); + } return f.getBytes(6, 90).then(buf => { - expect(buf.byteLength).to.equal(5); - expect(bufferToText(buf)).to.equal('6789\n'); + expect(buf).to.not.be.null; + if (buf != null) { + expect(buf.byteLength).to.equal(5); + expect(bufferToText(buf)).to.equal('6789\n'); + } }); }); }); - it('should fetch entire files as strings', function() { + it('should fetch entire files as strings', function(): any { var f = new LocalStringFile('0123456789\n'); return f.getAllString().then(txt => { expect(txt).to.equal('0123456789\n'); diff --git a/src/test/MappedRemoteFile-test.js b/src/test/MappedRemoteFile-test.js index d3e89e64..beefc3a8 100644 --- a/src/test/MappedRemoteFile-test.js +++ b/src/test/MappedRemoteFile-test.js @@ -13,14 +13,14 @@ describe('MappedRemoteFile', function() { return new jBinary(buf).read('string'); } - it('should serve requests through the map', function() { + it('should serve requests through the map', function(): any { var remoteFile = new MappedRemoteFile('/test-data/0to9.txt', [ [0, 2], // 0,1,2 [12345678, 12345680], // 3,4,5 [9876543210, 9876543214] // 6,7,8,9,\n ]); - var promises = [ + return Q.all([ remoteFile.getBytes(0, 3).then(buf => { expect(bufferToText(buf)).to.equal('012'); }), @@ -44,12 +44,10 @@ describe('MappedRemoteFile', function() { }, err => { expect(err).to.match(/is not mapped/); }), - ]; - - return Q.all(promises); + ]); }); - it('should forget file length', function() { + it('should forget file length', function(): any { var remoteFile = new MappedRemoteFile('/test-data/0to9.txt', [ [0, 2], // 0,1,2 [12345673, 12345690] // 3456789\n diff --git a/src/test/RemoteFile-test.js b/src/test/RemoteFile-test.js index 6885840d..a7e7af4c 100644 --- a/src/test/RemoteFile-test.js +++ b/src/test/RemoteFile-test.js @@ -12,7 +12,7 @@ describe('RemoteFile', () => { return new jBinary(buf).read('string'); } - it('should fetch a subset of a file', function() { + it('should fetch a subset of a file', function(): any { var f = new RemoteFile('/test-data/0to9.txt'); var promisedData = f.getBytes(4, 5); @@ -23,7 +23,7 @@ describe('RemoteFile', () => { }); }); - it('should fetch subsets from cache', function() { + it('should fetch subsets from cache', function(): any { var f = new RemoteFile('/test-data/0to9.txt'); return f.getBytes(0, 10).then(buf => { expect(buf.byteLength).to.equal(10); @@ -37,7 +37,7 @@ describe('RemoteFile', () => { }); }); - it('should fetch entire files', function() { + it('should fetch entire files', function(): any { var f = new RemoteFile('/test-data/0to9.txt'); return f.getAll().then(buf => { expect(buf.byteLength).to.equal(11); @@ -46,7 +46,7 @@ describe('RemoteFile', () => { }); }); - it('should determine file lengths', function() { + it('should determine file lengths', function(): any { var f = new RemoteFile('/test-data/0to9.txt'); return f.getSize().then(size => { expect(size).to.equal(11); @@ -55,7 +55,7 @@ describe('RemoteFile', () => { }); }); - it('should get file lengths from full requests', function() { + it('should get file lengths from full requests', function(): any { var f = new RemoteFile('/test-data/0to9.txt'); return f.getAll().then(buf => { expect(f.numNetworkRequests).to.equal(1); @@ -66,7 +66,7 @@ describe('RemoteFile', () => { }); }); - it('should get file lengths from range requests', function() { + it('should get file lengths from range requests', function(): any { var f = new RemoteFile('/test-data/0to9.txt'); return f.getBytes(4, 5).then(buf => { expect(f.numNetworkRequests).to.equal(1); @@ -77,9 +77,9 @@ describe('RemoteFile', () => { }); }); - it('should cache requests for full files', function() { + it('should cache requests for full files', function(): any { var f = new RemoteFile('/test-data/0to9.txt'); - f.getAll().then(buf => { + return f.getAll().then(buf => { expect(buf.byteLength).to.equal(11); expect(bufferToText(buf)).to.equal('0123456789\n'); expect(f.numNetworkRequests).to.equal(1); @@ -91,7 +91,7 @@ describe('RemoteFile', () => { }); }); - it('should serve range requests from cache after getAll', function() { + it('should serve range requests from cache after getAll', function(): any { var f = new RemoteFile('/test-data/0to9.txt'); return f.getAll().then(buf => { expect(buf.byteLength).to.equal(11); @@ -105,7 +105,7 @@ describe('RemoteFile', () => { }); }); - it('should reject requests to a non-existent file', function() { + it('should reject requests to a non-existent file', function(): any { var f = new RemoteFile('/test-data/nonexistent-file.txt'); return f.getAll().then(buf => { throw 'Requests for non-existent files should not succeed'; @@ -118,7 +118,7 @@ describe('RemoteFile', () => { }); }); - it('should truncate requests past EOF', function() { + it('should truncate requests past EOF', function(): any { var f = new RemoteFile('/test-data/0to9.txt'); var promisedData = f.getBytes(4, 100); @@ -134,7 +134,7 @@ describe('RemoteFile', () => { }); }); - it('should fetch entire files as strings', function() { + it('should fetch entire files as strings', function(): any { var f = new RemoteFile('/test-data/0to9.txt'); return f.getAllString().then(txt => { expect(txt).to.equal('0123456789\n'); diff --git a/src/test/RemoteRequest-test.js b/src/test/RemoteRequest-test.js index a693fd74..3ae9868f 100644 --- a/src/test/RemoteRequest-test.js +++ b/src/test/RemoteRequest-test.js @@ -15,14 +15,14 @@ describe('RemoteRequest', function() { var start = 10; var stop = 20; - before(function () { + before(function(): any { return new RemoteFile('/test-data/alignments.ga4gh.chr17.1-250.json').getAllString().then(data => { response = data; server = sinon.fakeServer.create(); }); }); - after(function () { + after(function() { server.restore(); }); diff --git a/src/test/SequenceStore-test.js b/src/test/SequenceStore-test.js index a0cf1444..e6864f28 100644 --- a/src/test/SequenceStore-test.js +++ b/src/test/SequenceStore-test.js @@ -8,73 +8,82 @@ import ContigInterval from '../main/ContigInterval'; describe('SequenceStore', function() { var ci = (contig, start, stop) => new ContigInterval(contig, start, stop); - it('should store sequences', function() { + it('should store sequences', function(done) { var store = new SequenceStore(); store.setRange(ci('chr1', 100, 109), 'ABCDEFGHIJ'); expect(store.getAsString(ci('chr1', 100, 109))).to.equal('ABCDEFGHIJ'); + done(); }); - it('should ignore chr-prefixes', function() { + it('should ignore chr-prefixes', function(done) { var store = new SequenceStore(); store.setRange(ci('chr1', 100, 104), 'ABCDE'); expect(store.getAsString(ci('1', 100, 104))).to.equal('ABCDE'); store.setRange(ci('2', 100, 103), 'WXYZ'); expect(store.getAsString(ci('chr2', 100, 103))).to.equal('WXYZ'); + done(); }); - it('should store and retrieve across chunk boundaries', function() { + it('should store and retrieve across chunk boundaries', function(done) { var store = new SequenceStore(); // 5678901234 store.setRange(ci('X', 995, 1004), 'ABCDEFGHIJ'); expect(store.getAsString(ci('X', 995, 1004))).to.equal('ABCDEFGHIJ'); + done(); }); - it('should add .s for unknown regions', function() { + it('should add .s for unknown regions', function(done) { var store = new SequenceStore(); expect(store.getAsString(ci('chr1', 9, 15))).to.equal('.......'); store.setRange(ci('chr1', 10, 14), 'ABCDE'); expect(store.getAsString(ci('chr1', 9, 15))).to.equal('.ABCDE.'); + done(); }); - it('should clobber previously-stored values', function() { + it('should clobber previously-stored values', function(done) { var store = new SequenceStore(); // 012345 store.setRange(ci('chr1', 10, 14), 'ABCDE'); store.setRange(ci('1', 13, 15), 'XYZ'); expect(store.getAsString(ci('chr1', 9, 16))).to.equal('.ABCXYZ.'); + done(); }); - it('should clobber across a boundary', function() { + it('should clobber across a boundary', function(done) { var store = new SequenceStore(); // 7890123 store.setRange(ci('chr1', 997, 1001), 'ABCDE'); store.setRange(ci('1', 999, 1002), 'XYZW'); expect(store.getAsString(ci('chr1', 996, 1003))).to.equal('.ABXYZW.'); + done(); }); - it('should store on a boundary', function() { + it('should store on a boundary', function(done) { var store = new SequenceStore(); store.setRange(ci('chr17', 1000, 1004), 'ABCDE'); expect(store.getAsString(ci('chr17', 1000, 1004))).to.equal('ABCDE'); + done(); }); - it('should store at a large position', function() { + it('should store at a large position', function(done) { var store = new SequenceStore(); store.setRange(ci('chr17', 123456789, 123456793), 'ABCDE'); expect(store.getAsString(ci('chr17', 123456788, 123456794))) .to.equal('.ABCDE.'); + done(); }); - it('should write across three chunks', function() { + it('should write across three chunks', function(done) { var store = new SequenceStore(); store.setRange(ci('X', 500, 2499), 'ABCDE' + 'N'.repeat(1990) + 'FGHIJ'); expect(store.getAsString(ci('X', 499, 505))).to.equal('.ABCDEN'); expect(store.getAsString(ci('X', 2494, 2500))).to.equal('NFGHIJ.'); expect(store.getAsString(ci('X', 499, 2500))).to.have.length(2002); + done(); }); - it('should return objects', function() { + it('should return objects', function(done) { var store = new SequenceStore(); store.setRange(ci('X', 10, 12), 'ABC'); expect(store.getAsObjects(ci('X', 9, 12))).to.deep.equal({ @@ -83,5 +92,6 @@ describe('SequenceStore', function() { 'X:11': 'B', 'X:12': 'C' }); + done(); }); }); diff --git a/src/test/async.js b/src/test/async.js index 2005b152..f4fe3740 100644 --- a/src/test/async.js +++ b/src/test/async.js @@ -9,7 +9,7 @@ import Q from 'q'; var WAIT_FOR_POLL_INTERVAL_MS = 100; // Returns a promise which resolves when predFn() is truthy. -function waitFor(predFn: () => boolean, timeoutMs: number): Q.Promise { +function waitFor(predFn: () => boolean, timeoutMs: number): Q.Promise<(T | null)>{ var def = Q.defer(); var checkTimeoutId = null; diff --git a/src/test/components-test.js b/src/test/components-test.js index d710abed..3491e234 100644 --- a/src/test/components-test.js +++ b/src/test/components-test.js @@ -57,6 +57,7 @@ describe('pileup', function() { ]; var testDiv = document.getElementById('testdiv'); + if (!testDiv) throw new Error("Failed to match: testdiv"); beforeEach(() => { dataCanvas.RecordingContext.recordAll(); // record all data canvases @@ -67,7 +68,7 @@ describe('pileup', function() { testDiv.innerHTML = ''; // avoid pollution between tests. }); - it('should render reference genome and genes', function() { + it('should render reference genome and genes', function(): any { this.timeout(5000); var div = document.createElement('div'); @@ -81,9 +82,9 @@ describe('pileup', function() { var {drawnObjects, drawnObjectsWith, callsOf} = dataCanvas.RecordingContext; - var uniqDrawnObjectsWith = function() { + var uniqDrawnObjectsWith = function(div: any, name: string, f: any) { return _.uniq( - drawnObjectsWith.apply(null, arguments), + drawnObjectsWith(div, name, f), false, // not sorted x => x.key); }; @@ -93,7 +94,8 @@ describe('pileup', function() { return div.querySelector(selector + ' canvas') && drawnObjects(div, selector).length > 0; } - var ready = (() => + var ready = ((): boolean => + // $FlowIgnore: TODO remove flow suppression hasCanvasAndObjects(div, '.reference') && hasCanvasAndObjects(div, '.variants') && hasCanvasAndObjects(div, '.genes') && @@ -118,10 +120,33 @@ describe('pileup', function() { // Note: there are 11 exons, but two are split into coding/non-coding expect(callsOf(div, '.genes', 'fillRect')).to.have.length(13); - expect(div.querySelector('div > .a').className).to.equal('track reference a'); - expect(div.querySelector('div > .b').className).to.equal('track variants b'); - expect(div.querySelector('div > .c').className).to.equal('track genes c'); - expect(div.querySelector('div > .d').className).to.equal('track pileup d'); + // check for reference + var selectedClass = div.querySelector('div > .a'); + expect(selectedClass).to.not.be.null; + if (selectedClass != null) { + expect(selectedClass.className).to.equal('track reference a'); + } + + // check for variants + selectedClass = div.querySelector('div > .b'); + expect(selectedClass).to.not.be.null; + if (selectedClass != null) { + expect(selectedClass.className).to.equal('track variants b'); + } + + // check for genes + selectedClass = div.querySelector('div > .c'); + expect(selectedClass).to.not.be.null; + if (selectedClass != null) { + expect(selectedClass.className).to.equal('track genes c'); + } + + // check for pileup + selectedClass = div.querySelector('div > .d'); + expect(selectedClass).to.not.be.null; + if (selectedClass != null) { + expect(selectedClass.className).to.equal('track pileup d'); + } expect(p.getRange()).to.deep.equal({ contig: 'chr17', diff --git a/src/test/data/BigBed-test.js b/src/test/data/BigBed-test.js index abf1a7bc..df6c69be 100644 --- a/src/test/data/BigBed-test.js +++ b/src/test/data/BigBed-test.js @@ -9,45 +9,45 @@ import BigBed from '../../main/data/BigBed'; import ContigInterval from '../../main/ContigInterval'; describe('BigBed', function() { - function getTestBigBed() { - return new BigBed('/test-data/itemRgb.bb'); // See test-data/README.md + function getTestBigBed () { + return new BigBed('/test-data/itemRgb.bb'); // See test-data/README.md } - function getUncompressedTestBigBed() { - return new BigBed('/test-data/simple17unc.bb'); // See test-data/README.md + function getUncompressedTestBigBed () { + return new BigBed('/test-data/simple17unc.bb'); // See test-data/README.md } - it('should extract features in a range', function() { + it('should extract features in a range', function(): any { var bb = getTestBigBed(); return bb.getFeaturesInRange('chrX', 151077036, 151078532) - .then(features => { - // Here's what these two lines in the file look like: - // chrX 151077031 151078198 MID_BLUE 0 - 151077031 151078198 0,0,128 - // chrX 151078198 151079365 VIOLET_RED1 0 - 151078198 151079365 255,62,150 - expect(features).to.have.length(2); - expect(features[0].contig).to.equal('chrX'); - expect(features[0].start).to.equal(151077031); - expect(features[0].stop).to.equal(151078198); - expect(features[1].contig).to.equal('chrX'); - expect(features[1].start).to.equal(151078198); - expect(features[1].stop).to.equal(151079365); - - var rest0 = features[0].rest.split('\t'); - expect(rest0).to.have.length(6); - expect(rest0[0]).to.equal('MID_BLUE'); - expect(rest0[2]).to.equal('-'); - expect(rest0[5]).to.equal('0,0,128'); - - var rest1 = features[1].rest.split('\t'); - expect(rest1).to.have.length(6); - expect(rest1[0]).to.equal('VIOLET_RED1'); - expect(rest1[2]).to.equal('-'); - expect(rest1[5]).to.equal('255,62,150'); - }); + .then(features => { + // Here's what these two lines in the file look like: + // chrX 151077031 151078198 MID_BLUE 0 - 151077031 151078198 0,0,128 + // chrX 151078198 151079365 VIOLET_RED1 0 - 151078198 151079365 255,62,150 + expect(features).to.have.length(2); + expect(features[0].contig).to.equal('chrX'); + expect(features[0].start).to.equal(151077031); + expect(features[0].stop).to.equal(151078198); + expect(features[1].contig).to.equal('chrX'); + expect(features[1].start).to.equal(151078198); + expect(features[1].stop).to.equal(151079365); + + var rest0 = features[0].rest.split('\t'); + expect(rest0).to.have.length(6); + expect(rest0[0]).to.equal('MID_BLUE'); + expect(rest0[2]).to.equal('-'); + expect(rest0[5]).to.equal('0,0,128'); + + var rest1 = features[1].rest.split('\t'); + expect(rest1).to.have.length(6); + expect(rest1[0]).to.equal('VIOLET_RED1'); + expect(rest1[2]).to.equal('-'); + expect(rest1[5]).to.equal('255,62,150'); + }); }); - it('should extract features from an uncompressed BigBed', function () { + it('should extract features from an uncompressed BigBed', function(): any { var bb = getUncompressedTestBigBed(); return bb.getFeaturesInRange('chr17', 60000, 270000) @@ -66,48 +66,49 @@ describe('BigBed', function() { }); }); - it('should have inclusive ranges', function() { + it('should have inclusive ranges', function(): any { // The matches looks like this: // chrX 151071196 151072363 RED // chrX 151094536 151095703 PeachPuff - var red = [151071196, 151072362]; // note: stop is inclusive + var red = [151071196, 151072362]; // note: stop is inclusive var bb = getTestBigBed(); var expectN = n => features => { - expect(features).to.have.length(n); - }; + expect(features).to.have.length(n); + }; return Q.all([ - // request for precisely one row from the file. - bb.getFeaturesInRange('chrX', red[0], red[1]) - .then(expectN(1)), - // the additional base in the range hits another row. - bb.getFeaturesInRange('chrX', red[0], 1 + red[1]) - .then(expectN(2)), - // this overlaps exactly one base pair of the first feature. - bb.getFeaturesInRange('chrX', red[0] - 1000, red[0]) - .then(expectN(1)), - // but this range ends one base pair before it. - bb.getFeaturesInRange('chrX', red[0] - 1000, red[0] - 1) - .then(expectN(0)) + // request for precisely one row from the file. + bb.getFeaturesInRange('chrX', red[0], red[1]) + .then(expectN(1)), + // the additional base in the range hits another row. + bb.getFeaturesInRange('chrX', red[0], 1 + red[1]) + .then(expectN(2)), + // this overlaps exactly one base pair of the first feature. + bb.getFeaturesInRange('chrX', red[0] - 1000, red[0]) + .then(expectN(1)), + // but this range ends one base pair before it. + bb.getFeaturesInRange('chrX', red[0] - 1000, red[0] - 1) + .then(expectN(0)) ]); }); - it('should add "chr" to contig names', function() { + it('should add "chr" to contig names', function(): any { var bb = getTestBigBed(); return bb.getFeaturesInRange('X', 151077036, 151078532) - .then(features => { - // (same as 'should extract features in a range' test) - expect(features).to.have.length(2); - expect(features[0].contig).to.equal('chrX'); - expect(features[1].contig).to.equal('chrX'); - }); + .then(features => { + // (same as 'should extract features in a range' test) + expect(features).to.have.length(2); + expect(features[0].contig).to.equal('chrX'); + expect(features[1].contig).to.equal('chrX'); + }); }); - it('should cache requests in a block', function() { - var bb = getTestBigBed(), - remote = bb.remoteFile; + it('should cache requests in a block', function(): any { + var bb = getTestBigBed(); + + var remote = bb.remoteFile; return bb.getFeaturesInRange('X', 151077036, 151078532).then(() => { // cache has been warmed up -- flush it to get a deterministic test. remote.clearCache(); @@ -131,18 +132,19 @@ describe('BigBed', function() { }); }); - it('should fetch full blocks', function() { + it('should fetch full blocks', function(): any { var bb = getTestBigBed(); var range = new ContigInterval('X', 151077036, 151078532); return bb.getFeatureBlocksOverlapping(range) - .then(blockFeatures => { - expect(blockFeatures).to.have.length(1); // just one block fetched. - var range = blockFeatures[0].range, - rows = blockFeatures[0].rows; - expect(rows).to.have.length(21); // all the chrX features. - expect(range.toString()).to.equal('chrX:151071196-151095703'); - }); + .then(blockFeatures => { + expect(blockFeatures).to.have.length(1); // just one block fetched. + var range = blockFeatures[0].range; + + var rows = blockFeatures[0].rows; + expect(rows).to.have.length(21); // all the chrX features. + expect(range.toString()).to.equal('chrX:151071196-151095703'); + }); }); // Things left to test: diff --git a/src/test/data/SamRead-test.js b/src/test/data/SamRead-test.js index 9954e393..764936db 100644 --- a/src/test/data/SamRead-test.js +++ b/src/test/data/SamRead-test.js @@ -1,11 +1,11 @@ /* @flow */ 'use strict'; -import type Q from 'q'; -import type SamRead from '../../main/data/SamRead'; - import {expect} from 'chai'; +import Q from 'q'; +import type SamRead from '../../main/data/SamRead'; + import RemoteFile from '../../main/RemoteFile'; import Bam from '../../main/data/bam'; import ContigInterval from '../../main/ContigInterval'; @@ -19,13 +19,13 @@ describe('SamRead', function() { var testReads = getSamArray('/test-data/test_input_1_a.bam'); // This is more of a test for the test than for SamRead. - it('should pull records from a BAM file', function() { + it('should pull records from a BAM file', function(): any { return testReads.then(reads => { expect(reads).to.have.length(15); }); }); - it('should parse BAM records', function() { + it('should parse BAM records', function(): any { return testReads.then(reads => { // The first record in test_input_1_a.sam is: // r000 99 insert 50 30 10M = 80 30 ATTTAGCTAC AAAAAAAAAA RG:Z:cow PG:Z:bull @@ -60,7 +60,7 @@ describe('SamRead', function() { }); }); - it('should read thick records', function() { + it('should read thick records', function(): any { return testReads.then(reads => { // This mirrors the "BAM > should parse BAM files" test. var r000 = reads[0].getFull(); @@ -89,7 +89,7 @@ describe('SamRead', function() { }); }); - it('should find record intersections', function() { + it('should find record intersections', function(): any { return testReads.then(reads => { var read = reads[0]; // toString() produces a 1-based result, but ContigInterval is 0-based. diff --git a/src/test/data/TwoBit-test.js b/src/test/data/TwoBit-test.js index 0e696a58..8f789396 100644 --- a/src/test/data/TwoBit-test.js +++ b/src/test/data/TwoBit-test.js @@ -7,46 +7,46 @@ import TwoBit from '../../main/data/TwoBit'; import RemoteFile from '../../main/RemoteFile'; describe('TwoBit', function() { - function getTestTwoBit() { + function getTestTwoBit () { // See test/data/README.md for provenance return new TwoBit(new RemoteFile('/test-data/test.2bit')); } - it('should have the right contigs', function() { + it('should have the right contigs', function(): any { var twoBit = getTestTwoBit(); return twoBit.getContigList() - .then(contigs => { - expect(contigs).to.deep.equal(['chr1', 'chr17', 'chr22']); - }); + .then(contigs => { + expect(contigs).to.deep.equal(['chr1', 'chr17', 'chr22']); + }); }); - it('should extract unknowns', function() { + it('should extract unknowns', function(): any { // This test mirrors dalliance's (chr22:19178140-19178170) var twoBit = getTestTwoBit(); return twoBit.getFeaturesInRange('chr22', 0, 30) - .then(basePairs => { - expect(basePairs).to.equal('NTCACAGATCACCATACCATNTNNNGNNCNA'); - }); + .then(basePairs => { + expect(basePairs).to.equal('NTCACAGATCACCATACCATNTNNNGNNCNA'); + }); }); - it('should reject invalid contigs', function() { + it('should reject invalid contigs', function(): any { var twoBit = getTestTwoBit(); return twoBit.getFeaturesInRange('chrZ', 12, 34) - .then(() => { assert.fail('Should have thrown'); }) - .catch(err => { - expect(err).to.match(/Invalid contig/); - }); + .then(() => { assert.fail('Should have thrown'); }) + .catch(err => { + expect(err).to.match(/Invalid contig/); + }); }); - it('should add chr', function() { + it('should add chr', function(): any { var twoBit = getTestTwoBit(); - return twoBit.getFeaturesInRange('22', 0, 4) // 22, not chr22 - .then(basePairs => { - expect(basePairs).to.equal('NTCAC'); - }); + return twoBit.getFeaturesInRange('22', 0, 4) // 22, not chr22 + .then(basePairs => { + expect(basePairs).to.equal('NTCAC'); + }); }); - it('should parse huge headers', function() { + it('should parse huge headers', function(): any { var twoBit = new TwoBit(new RemoteFile('/test-data/susScr3-head.2bit')); // shouldn't throw an exception return twoBit.header.then(header => { diff --git a/src/test/data/VirtualOffset-test.js b/src/test/data/VirtualOffset-test.js index f31dc0a5..7184b3a1 100644 --- a/src/test/data/VirtualOffset-test.js +++ b/src/test/data/VirtualOffset-test.js @@ -10,40 +10,44 @@ import VirtualOffset from '../../main/data/VirtualOffset'; describe('VirtualOffset', function() { // These test that .fromBlob() is equivalent to jBinary.read('VirtualOffset'). // They match tests in bai-test.js - it('should read directly from buffers', function() { + it('should read directly from buffers', function(done) { var u8 = new Uint8Array([201, 121, 79, 100, 96, 92, 1, 0]); - var vjBinary = new jBinary(u8, bamTypes.TYPE_SET).read('VirtualOffset'), - vDirect = VirtualOffset.fromBlob(u8); + var vjBinary = new jBinary(u8, bamTypes.TYPE_SET).read('VirtualOffset'); + + var vDirect = VirtualOffset.fromBlob(u8); expect(vDirect.toString()).to.equal(vjBinary.toString()); u8 = new Uint8Array([218, 128, 112, 239, 7, 0, 0, 0]); vjBinary = new jBinary(u8, bamTypes.TYPE_SET).read('VirtualOffset'); vDirect = VirtualOffset.fromBlob(u8); expect(vDirect.toString()).to.equal(vjBinary.toString()); + done(); }); - it('should read with an offset', function() { + it('should read with an offset', function(done) { var base = new Uint8Array( - [0, 1, 2, 3, 4, 5, 6, 7, - 86, 5, 10, 214, 117, 169, 37, 0, - 86, 5, 10, 214, 117, 169, 37, 0, - 86, 5, 10, 214, 117, 169, 37, 0, - 200, 6, 10, 214, 117, 169, 37, 0] + [0, 1, 2, 3, 4, 5, 6, 7, + 86, 5, 10, 214, 117, 169, 37, 0, + 86, 5, 10, 214, 117, 169, 37, 0, + 86, 5, 10, 214, 117, 169, 37, 0, + 200, 6, 10, 214, 117, 169, 37, 0] ); - var u8 = new Uint8Array(base.buffer, 8); // this is offset from base + var u8 = new Uint8Array(base.buffer, 8); // this is offset from base + + var vjBinary = new jBinary(u8, bamTypes.TYPE_SET).read('IntervalsArray'); - var vjBinary = new jBinary(u8, bamTypes.TYPE_SET).read('IntervalsArray'), - vDirect = [ - VirtualOffset.fromBlob(u8, 0), - VirtualOffset.fromBlob(u8, 8), - VirtualOffset.fromBlob(u8, 16), - VirtualOffset.fromBlob(u8, 24) - ]; + var vDirect = [ + VirtualOffset.fromBlob(u8, 0), + VirtualOffset.fromBlob(u8, 8), + VirtualOffset.fromBlob(u8, 16), + VirtualOffset.fromBlob(u8, 24) + ]; expect(vjBinary).to.have.length(4); expect(vDirect[0].toString()).to.equal(vjBinary[0].toString()); expect(vDirect[1].toString()).to.equal(vjBinary[1].toString()); expect(vDirect[2].toString()).to.equal(vjBinary[2].toString()); expect(vDirect[3].toString()).to.equal(vjBinary[3].toString()); + done(); }); }); diff --git a/src/test/data/bai-test.js b/src/test/data/bai-test.js index 98f68de7..fb87ee25 100644 --- a/src/test/data/bai-test.js +++ b/src/test/data/bai-test.js @@ -11,20 +11,21 @@ import ContigInterval from '../../main/ContigInterval'; import RemoteFile from '../../main/RemoteFile'; import RecordedRemoteFile from '../RecordedRemoteFile'; -function chunkToString(chunk) { - return `${chunk.chunk_beg}-${chunk.chunk_end}`; +function chunkToString (chunk) { + return `${chunk.chunk_beg.toString()}-${chunk.chunk_end.toString()}`; } describe('BAI', function() { - it('should parse virtual offsets', function() { + it('should parse virtual offsets', function(done) { var u8 = new Uint8Array([201, 121, 79, 100, 96, 92, 1, 0]); var vo = new jBinary(u8, bamTypes.TYPE_SET).read('VirtualOffset'); // (expected values from dalliance) expect(vo.uoffset).to.equal(31177); expect(vo.coffset).to.equal(5844788303); + done(); }); - it('should parse virtual offsets near 2^32', function() { + it('should parse virtual offsets near 2^32', function(done) { // The low 32 bits of these virtual offsets are in [2^31, 2^32], which // could cause sign propagation bugs with incorrect implementations. var u8 = new Uint8Array([218, 128, 112, 239, 7, 0, 0, 0]); @@ -34,10 +35,11 @@ describe('BAI', function() { u8 = new Uint8Array([230, 129, 112, 239, 7, 0, 0, 0]); vo = new jBinary(u8, bamTypes.TYPE_SET).read('VirtualOffset'); expect(vo.toString()).to.equal('520048:33254'); + done(); }); // This matches htsjdk's BamFileIndexTest.testSpecificQueries - it('should parse large BAI files', function() { + it('should parse large BAI files', function(): any { var bai = new BaiFile(new RemoteFile('/test-data/index_test.bam.bai')); // contig 0 = chrM @@ -48,13 +50,13 @@ describe('BAI', function() { }); }); - it('should use index chunks', function() { + it('should use index chunks', function(): any { var remoteFile = new RecordedRemoteFile('/test-data/index_test.bam.bai'); var bai = new BaiFile(remoteFile, - { - 'chunks': [[8, 144], [144, 13776]], - 'minBlockIndex': 65536 - }); + { + 'chunks': [[8, 144], [144, 13776]], + 'minBlockIndex': 65536 + }); // contig 0 = chrM var range = new ContigInterval(0, 10400, 10600); @@ -68,7 +70,7 @@ describe('BAI', function() { }); }); - it('should compute index chunks', function() { + it('should compute index chunks', function(): any { var bai = new BaiFile(new RemoteFile('/test-data/index_test.bam.bai')); return bai.immediate.then(imm => { var chunks = imm.indexChunks; @@ -127,7 +129,7 @@ describe('BAI', function() { }); }); - it('should index a small BAI file', function() { + it('should index a small BAI file', function(): any { var bai = new BaiFile(new RemoteFile('/test-data/test_input_1_b.bam.bai')); return bai.immediate.then(imm => { var chunks = imm.indexChunks; diff --git a/src/test/data/bam-test.js b/src/test/data/bam-test.js index f3286468..771dde26 100644 --- a/src/test/data/bam-test.js +++ b/src/test/data/bam-test.js @@ -10,7 +10,7 @@ import MappedRemoteFile from '../MappedRemoteFile'; import VirtualOffset from '../../main/data/VirtualOffset'; describe('BAM', function() { - it('should parse BAM files', function() { + it('should parse BAM files', function(): any { var bamFile = new Bam(new RemoteFile('/test-data/test_input_1_a.bam')); return bamFile.readAll().then(bamData => { var refs = bamData.header.references; @@ -55,16 +55,15 @@ describe('BAM', function() { // This one has a more interesting Cigar string expect(aligns[3].getCigarString()) - .to.equal('1S2I6M1P1I1P1I4M2I'); - + .to.equal('1S2I6M1P1I1P1I4M2I'); // - one with a more interesting Phred string }); }); // This matches htsjdk's BamFileIndexTest.testSpecificQueries - it('should find sequences using an index', function() { + it('should find sequences using an index', function(): any { var bam = new Bam(new RemoteFile('/test-data/index_test.bam'), - new RemoteFile('/test-data/index_test.bam.bai')); + new RemoteFile('/test-data/index_test.bam.bai')); // TODO: run these in parallel var range = new ContigInterval('chrM', 10400, 10600); @@ -83,9 +82,9 @@ describe('BAM', function() { }); }); - it('should fetch alignments from chr18', function() { + it('should fetch alignments from chr18', function(): any { var bam = new Bam(new RemoteFile('/test-data/index_test.bam'), - new RemoteFile('/test-data/index_test.bam.bai')); + new RemoteFile('/test-data/index_test.bam.bai')); var range = new ContigInterval('chr18', 3627238, 6992285); /* Grabbed from IntelliJ & htsjdk using this code fragment: @@ -101,27 +100,27 @@ describe('BAM', function() { // Note: htsjdk returns contig names like 'chr18', not 18. expect(reads).to.have.length(14); expect(reads.map(r => r.toString())).to.deep.equal([ - 'chr18:3653516-3653566', - 'chr18:3653591-3653641', - 'chr18:4215486-4215536', - 'chr18:4215629-4215679', - 'chr18:4782331-4782381', - 'chr18:4782490-4782540', - 'chr18:5383914-5383964', - 'chr18:5384093-5384143', - 'chr18:5904078-5904128', - 'chr18:5904241-5904291', - 'chr18:6412181-6412231', - 'chr18:6412353-6412403', - 'chr18:6953238-6953288', - 'chr18:6953412-6953462' + 'chr18:3653516-3653566', + 'chr18:3653591-3653641', + 'chr18:4215486-4215536', + 'chr18:4215629-4215679', + 'chr18:4782331-4782381', + 'chr18:4782490-4782540', + 'chr18:5383914-5383964', + 'chr18:5384093-5384143', + 'chr18:5904078-5904128', + 'chr18:5904241-5904291', + 'chr18:6412181-6412231', + 'chr18:6412353-6412403', + 'chr18:6953238-6953288', + 'chr18:6953412-6953462' ]); }); }); - it('should fetch alignments across a chunk boundary', function() { + it('should fetch alignments across a chunk boundary', function(): any { var bam = new Bam(new RemoteFile('/test-data/index_test.bam'), - new RemoteFile('/test-data/index_test.bam.bai')); + new RemoteFile('/test-data/index_test.bam.bai')); var range = new ContigInterval('chr1', 90002285, 116992285); return bam.getAlignmentsInRange(range).then(reads => { expect(reads).to.have.length(92); @@ -146,7 +145,7 @@ describe('BAM', function() { }); }); - it('should fetch an alignment at a specific offset', function() { + it('should fetch an alignment at a specific offset', function(): any { // This virtual offset matches the one above. // This verifies that alignments are tagged with the correct offset. var bam = new Bam(new RemoteFile('/test-data/index_test.bam')); @@ -155,9 +154,9 @@ describe('BAM', function() { }); }); - it('should fetch alignments in a wide interval', function() { + it('should fetch alignments in a wide interval', function(): any { var bam = new Bam(new RemoteFile('/test-data/index_test.bam'), - new RemoteFile('/test-data/index_test.bam.bai')); + new RemoteFile('/test-data/index_test.bam.bai')); var range = new ContigInterval('chr20', 1, 412345678); return bam.getAlignmentsInRange(range).then(reads => { // This count matches what you get if you run: @@ -166,14 +165,15 @@ describe('BAM', function() { }); }); - it('should fetch from a large, dense BAM file', function() { + it('should fetch from a large, dense BAM file', function(): any { this.timeout(5000); // See test/data/README.md for details on where these files came from. var remoteBAI = new MappedRemoteFile('/test-data/dream.synth3.bam.bai.mapped', - [[8054040, 8242920]]), - remoteBAM = new MappedRemoteFile('/test-data/dream.synth3.bam.mapped', - [[0, 69453], [163622109888, 163622739903]]); + [[8054040, 8242920]]); + + var remoteBAM = new MappedRemoteFile('/test-data/dream.synth3.bam.mapped', + [[0, 69453], [163622109888, 163622739903]]); var bam = new Bam(remoteBAM, remoteBAI, { // "chunks" is usually an array; here we take advantage of the @@ -192,10 +192,10 @@ describe('BAM', function() { }); // Regression test for https://github.com/hammerlab/pileup.js/issues/88 - it('should fetch reads at EOF', function() { - var bamFile = new RemoteFile('/test-data/synth3.normal.17.7500000-7515000.bam'), - baiFile = new RemoteFile('/test-data/synth3.normal.17.7500000-7515000.bam.bai'), - bam = new Bam(bamFile, baiFile); + it('should fetch reads at EOF', function(): any { + var bamFile = new RemoteFile('/test-data/synth3.normal.17.7500000-7515000.bam'); + var baiFile = new RemoteFile('/test-data/synth3.normal.17.7500000-7515000.bam.bai'); + var bam = new Bam(bamFile, baiFile); var range = new ContigInterval('chr17', 7514800, 7515100); return bam.getAlignmentsInRange(range).then(reads => { @@ -206,11 +206,11 @@ describe('BAM', function() { // Regression test for https://github.com/hammerlab/pileup.js/issues/172 // TODO: find a simpler BAM which exercises this code path. - it('should progress through the chunk list', function() { - var bamFile = new MappedRemoteFile('/test-data/small-chunks.bam.mapped', [[0, 65535], [6536374255, 6536458689], [6536533365, 6536613506], [6536709837, 6536795141]]), - baiFile = new MappedRemoteFile('/test-data/small-chunks.bam.bai.mapped', [[6942576, 7102568]]), - chunks = {'chunks': {'19': [6942576, 7102568]}, 'minBlockIndex': 65536}, - bam = new Bam(bamFile, baiFile, chunks); + it('should progress through the chunk list', function(): any { + var bamFile = new MappedRemoteFile('/test-data/small-chunks.bam.mapped', [[0, 65535], [6536374255, 6536458689], [6536533365, 6536613506], [6536709837, 6536795141]]); + var baiFile = new MappedRemoteFile('/test-data/small-chunks.bam.bai.mapped', [[6942576, 7102568]]); + var chunks = {'chunks': {'19': [6942576, 7102568]}, 'minBlockIndex': 65536}; + var bam = new Bam(bamFile, baiFile, chunks); var range = new ContigInterval('chr20', 2684600, 2684800); return bam.getAlignmentsInRange(range).then(reads => { @@ -218,11 +218,11 @@ describe('BAM', function() { }); }); - it('should fire progress events', function() { - var bamFile = new MappedRemoteFile('/test-data/small-chunks.bam.mapped', [[0, 65535], [6536374255, 6536458689], [6536533365, 6536613506], [6536709837, 6536795141]]), - baiFile = new MappedRemoteFile('/test-data/small-chunks.bam.bai.mapped', [[6942576, 7102568]]), - chunks = {'chunks': {'19': [6942576, 7102568]}, 'minBlockIndex': 65536}, - bam = new Bam(bamFile, baiFile, chunks); + it('should fire progress events', function(): any { + var bamFile = new MappedRemoteFile('/test-data/small-chunks.bam.mapped', [[0, 65535], [6536374255, 6536458689], [6536533365, 6536613506], [6536709837, 6536795141]]); + var baiFile = new MappedRemoteFile('/test-data/small-chunks.bam.bai.mapped', [[6942576, 7102568]]); + var chunks = {'chunks': {'19': [6942576, 7102568]}, 'minBlockIndex': 65536}; + var bam = new Bam(bamFile, baiFile, chunks); var range = new ContigInterval('chr20', 2684600, 2684800); var progressEvents = []; diff --git a/src/test/data/bedtools-test.js b/src/test/data/bedtools-test.js index e2830e9f..6e325419 100644 --- a/src/test/data/bedtools-test.js +++ b/src/test/data/bedtools-test.js @@ -11,7 +11,7 @@ describe('bedtools', function() { var splitCodingExons = bedtools.splitCodingExons; var CodingInterval = bedtools.CodingInterval; - it('should split one exon', function() { + it('should split one exon', function(done) { var exon = new Interval(10, 20); expect(splitCodingExons([exon], new Interval(13, 17))).to.deep.equal([ @@ -39,9 +39,10 @@ describe('bedtools', function() { new CodingInterval(10, 14, false), new CodingInterval(15, 20, true) ]); + done(); }); - it('should handle purely coding or non-coding exons', function() { + it('should handle purely coding or non-coding exons', function(done) { var exon = new Interval(10, 20); expect(splitCodingExons([exon], new Interval(0, 9))).to.deep.equal([ @@ -53,6 +54,7 @@ describe('bedtools', function() { expect(splitCodingExons([exon], new Interval(10, 20))).to.deep.equal([ new CodingInterval(10, 20, true) ]); + done(); }); }); }); diff --git a/src/test/data/feature-test.js b/src/test/data/feature-test.js index 735444a3..b326596c 100644 --- a/src/test/data/feature-test.js +++ b/src/test/data/feature-test.js @@ -9,24 +9,23 @@ import RemoteFile from '../../main/RemoteFile'; describe('Feature', function() { var json; - before(function () { + before(function(): any { return new RemoteFile('/test-data/features.ga4gh.chr1.120000-125000.json').getAllString().then(data => { json = data; }); }); it('should parse features from GA4GH', function(done) { + // parse json + var parsedJson = JSON.parse(json); + var features = _.values(parsedJson.features).map(feature => Feature.fromGA4GH(feature)); - // parse json - var parsedJson = JSON.parse(json); - var features = _.values(parsedJson.features).map(feature => Feature.fromGA4GH(feature)); - - expect(features).to.have.length(9); - expect(features[0].position.contig).to.equal("chr1"); - expect(features[0].position.start()).to.equal(89295); - expect(features[0].position.stop()).to.equal(120932); - expect(features[0].id).to.equal("WyIxa2dlbm9tZXMiLCJnZW5jb2RlX3YyNGxpZnQzNyIsIjE0MDUwOTE3MjM1NDE5MiJd"); - expect(features[0].score).to.equal(1000); - done(); + expect(features).to.have.length(9); + expect(features[0].position.contig).to.equal("chr1"); + expect(features[0].position.start()).to.equal(89295); + expect(features[0].position.stop()).to.equal(120932); + expect(features[0].id).to.equal("WyIxa2dlbm9tZXMiLCJnZW5jb2RlX3YyNGxpZnQzNyIsIjE0MDUwOTE3MjM1NDE5MiJd"); + expect(features[0].score).to.equal(1000); + done(); }); }); diff --git a/src/test/data/variant-test.js b/src/test/data/variant-test.js index a09c302f..70515888 100644 --- a/src/test/data/variant-test.js +++ b/src/test/data/variant-test.js @@ -9,23 +9,22 @@ import RemoteFile from '../../main/RemoteFile'; describe('Variant', function() { var json; - before(function () { + before(function(): any { return new RemoteFile('/test-data/variants.ga4gh.chr1.10000-11000.json').getAllString().then(data => { json = data; }); }); it('should parse variants from GA4GH', function(done) { + // parse json + var parsedJson = JSON.parse(json); + var variants = _.values(parsedJson.variants).map(variant => Variant.fromGA4GH(variant)); - // parse json - var parsedJson = JSON.parse(json); - var variants = _.values(parsedJson.variants).map(variant => Variant.fromGA4GH(variant)); - - expect(variants).to.have.length(11); - expect(variants[0].contig).to.equal("1"); - expect(variants[0].position).to.equal(10176); - expect(variants[0].ref).to.equal("A"); - expect(variants[0].alt[0]).to.equal("AC"); - done(); + expect(variants).to.have.length(11); + expect(variants[0].contig).to.equal("1"); + expect(variants[0].position).to.equal(10176); + expect(variants[0].ref).to.equal("A"); + expect(variants[0].alt[0]).to.equal("AC"); + done(); }); }); diff --git a/src/test/data/vcf-test.js b/src/test/data/vcf-test.js index 02ad862c..2ae691cb 100644 --- a/src/test/data/vcf-test.js +++ b/src/test/data/vcf-test.js @@ -9,14 +9,15 @@ import RemoteFile from '../../main/RemoteFile'; import LocalStringFile from '../../main/LocalStringFile'; describe('VCF', function() { - describe('should respond to queries', function() { + describe('should respond to queries', function(): any { var testQueries = (vcf) => { var range = new ContigInterval('20', 63799, 69094); return vcf.getFeaturesInRange(range).then(features => { expect(features).to.have.length(6); - var v0 = features[0], - v5 = features[5]; + var v0 = features[0]; + + var v5 = features[5]; expect(v0.contig).to.equal('20'); expect(v0.position).to.equal(63799); @@ -32,12 +33,13 @@ describe('VCF', function() { var remoteFile = new RemoteFile('/test-data/snv.vcf'); - it('remote file', function() { + it('remote file', function(done) { var vcf = new VcfFile(remoteFile); testQueries(vcf); + done(); }); - it('local file from string', function() { + it('local file from string', function(): any { return remoteFile.getAllString().then(content => { var localFile = new LocalStringFile(content); var vcf = new VcfFile(localFile); @@ -46,7 +48,7 @@ describe('VCF', function() { }); }); - it('should have frequency', function() { + it('should have frequency', function(): any { var vcf = new VcfFile(new RemoteFile('/test-data/allelFrequency.vcf')); var range = new ContigInterval('chr20', 61790, 61800); return vcf.getFeaturesInRange(range).then(features => { @@ -57,7 +59,7 @@ describe('VCF', function() { }); }); - it('should have highest frequency', function() { + it('should have highest frequency', function(): any { var vcf = new VcfFile(new RemoteFile('/test-data/allelFrequency.vcf')); var range = new ContigInterval('chr20', 61730, 61740); return vcf.getFeaturesInRange(range).then(features => { @@ -68,20 +70,22 @@ describe('VCF', function() { }); }); - it('should add chr', function() { + it('should add chr', function(): any { var vcf = new VcfFile(new RemoteFile('/test-data/snv.vcf')); var range = new ContigInterval('chr20', 63799, 69094); return vcf.getFeaturesInRange(range).then(features => { expect(features).to.have.length(6); - expect(features[0].contig).to.equal('20'); // not chr20 + expect(features[0].contig).to.equal('20'); // not chr20 expect(features[5].contig).to.equal('20'); }); }); - it('should handle unsorted VCFs', function() { + it('should handle unsorted VCFs', function(): any { var vcf = new VcfFile(new RemoteFile('/test-data/sort-bug.vcf')); - var chr1 = new ContigInterval('chr1', 1, 1234567890), // all of chr1 - chr5 = new ContigInterval('chr5', 1, 1234567890); + var chr1 = new ContigInterval('chr1', 1, 1234567890); + // all of chr1 + + var chr5 = new ContigInterval('chr5', 1, 1234567890); return vcf.getFeaturesInRange(chr1).then(features => { expect(features).to.have.length(5); return vcf.getFeaturesInRange(chr5); diff --git a/src/test/helpers-test.js b/src/test/helpers-test.js index cec173ec..485aa0d7 100644 --- a/src/test/helpers-test.js +++ b/src/test/helpers-test.js @@ -7,7 +7,7 @@ import jBinary from 'jbinary'; import helpers from '../main/data/formats/helpers'; describe('jBinary Helpers', function() { - it('should read sized blocks', function() { + it('should read sized blocks', function(done) { // 5 ------------- 3 ------- 4 ---------- var u8 = new Uint8Array([5, 1, 2, 3, 4, 5, 3, 1, 2, 3, 4, 1, 2, 3, 4]); var TYPE_SET = { @@ -25,18 +25,20 @@ describe('jBinary Helpers', function() { {length: 3, contents: [1, 2, 3]}, {length: 4, contents: [1, 2, 3, 4]} ]); + done(); }); - it('should read fixed-size null-terminated strings', function() { + it('should read fixed-size null-terminated strings', function(done) { // A B C D B, C var u8 = new Uint8Array([65, 66, 67, 68, 0, 66, 67, 0, 0, 0]); var jb = new jBinary(u8); var o = jb.read(['array', [helpers.nullString, 5]]); expect(o).to.deep.equal(['ABCD', 'BC']); + done(); }); - it('should read arrays of simple types lazily', function() { + it('should read arrays of simple types lazily', function(done) { var numReads = 0; var countingUint8 = jBinary.Template({ baseType: 'uint8', @@ -57,9 +59,10 @@ describe('jBinary Helpers', function() { expect(numReads).to.equal(2); expect(o.get(9)).to.equal(6); expect(numReads).to.equal(3); + done(); }); - it('should read arrays of objects lazily', function() { + it('should read arrays of objects lazily', function(done) { var u8 = new Uint8Array([65, 66, 67, 68, 1, 2, 3, 4, 5, 6]); var jb = new jBinary(u8); var o = jb.read([helpers.lazyArray, {x: 'uint8', y: 'uint8'}, 2, 5]); @@ -67,18 +70,20 @@ describe('jBinary Helpers', function() { expect(o.get(0)).to.deep.equal({x: 65, y: 66}); expect(o.get(1)).to.deep.equal({x: 67, y: 68}); expect(o.get(4)).to.deep.equal({x: 5, y: 6}); + done(); }); - it('should read the entire array lazily', function() { + it('should read the entire array lazily', function(done) { // A B C D B, C var u8 = new Uint8Array([65, 66, 67, 68, 0, 66, 67, 0, 0, 0]); var jb = new jBinary(u8); var o = jb.read([helpers.lazyArray, [helpers.nullString, 5], 5, 2]); expect(o.getAll()).to.deep.equal(['ABCD', 'BC']); + done(); }); - it('should read uint64s as native numbers', function() { + it('should read uint64s as native numbers', function(done) { var TYPE_SET = { 'jBinary.littleEndian': true, uint64native: helpers.uint64native @@ -90,5 +95,6 @@ describe('jBinary Helpers', function() { expect(() => u8big.read('uint64native')).to.throw(RangeError); expect(u8small.read('uint64native')).to.equal(21261123584); + done(); }); }); diff --git a/src/test/json/GA4GHAlignmentJson-test.js b/src/test/json/GA4GHAlignmentJson-test.js index d3e77372..db613074 100644 --- a/src/test/json/GA4GHAlignmentJson-test.js +++ b/src/test/json/GA4GHAlignmentJson-test.js @@ -10,7 +10,7 @@ import RemoteFile from '../../main/RemoteFile'; describe('GA4GHAlignmentJson', function() { var json; - before(function () { + before(function(): any { return new RemoteFile('/test-data/alignments.ga4gh.chr17.1-250.json').getAllString().then(data => { json = data; }); @@ -25,7 +25,7 @@ describe('GA4GHAlignmentJson', function() { var reads = source.getAlignmentsInRange(requestInterval); expect(reads).to.have.length(2); done(); - + }); it('should not fail on empty json string', function(done) { @@ -39,5 +39,4 @@ describe('GA4GHAlignmentJson', function() { done(); }); - }); diff --git a/src/test/json/GA4GHFeatureJson-test.js b/src/test/json/GA4GHFeatureJson-test.js index b1f3af7f..e012bc4c 100644 --- a/src/test/json/GA4GHFeatureJson-test.js +++ b/src/test/json/GA4GHFeatureJson-test.js @@ -10,14 +10,14 @@ import RemoteFile from '../../main/RemoteFile'; describe('GA4GHFeatureJson', function() { var json; - before(function () { + before(function(): any { return new RemoteFile('/test-data/features.ga4gh.chr1.120000-125000.json').getAllString().then(data => { json = data; }); }); it('should filter features from json', function(done) { - + var source = GA4GHFeatureJson.create(json); var requestInterval = new ContigInterval('chr1', 130000, 135000); diff --git a/src/test/json/GA4GHVariantJson-test.js b/src/test/json/GA4GHVariantJson-test.js index 3f71bc07..93ecb361 100644 --- a/src/test/json/GA4GHVariantJson-test.js +++ b/src/test/json/GA4GHVariantJson-test.js @@ -10,14 +10,14 @@ import RemoteFile from '../../main/RemoteFile'; describe('GA4GHVariantJson', function() { var json; - before(function () { + before(function(): any { return new RemoteFile('/test-data/variants.ga4gh.chr1.10000-11000.json').getAllString().then(data => { json = data; }); }); it('should filter variants from json', function(done) { - + var source = GA4GHVariantJson.create(json); var requestInterval = new ContigInterval('1', 10000, 10500); diff --git a/src/test/scale-test.js b/src/test/scale-test.js index 4eb72b74..6263f074 100644 --- a/src/test/scale-test.js +++ b/src/test/scale-test.js @@ -6,32 +6,36 @@ import {expect} from 'chai'; import scale from '../main/scale'; describe('scale', function() { - it('should define a linear scale', function() { + it('should define a linear scale', function(done) { var sc = scale.linear().domain([100, 201]).range([0, 1000]); expect(sc(100)).to.equal(0); expect(sc(201)).to.equal(1000); + done(); }); - it('should be invertible', function() { + it('should be invertible', function(done) { var sc = scale.linear().domain([100, 201]).range([0, 1000]); expect(sc.invert(0)).to.equal(100); expect(sc.invert(1000)).to.equal(201); + done(); }); - it('should be clampable', function() { + it('should be clampable', function(done) { var sc = scale.linear().domain([100, 201]).range([0, 1000]); sc = sc.clamp(true); expect(sc(0)).to.equal(0); expect(sc(100)).to.equal(0); expect(sc(201)).to.equal(1000); expect(sc(500)).to.equal(1000); + done(); }); - it('should have nice values', function() { + it('should have nice values', function(done) { var sc = scale.linear().domain([33, 0]).range(0, 100).nice(); expect(sc.domain()).to.deep.equal([35, 0]); sc = scale.linear().domain([0, 33]).range(0, 100).nice(); expect(sc.domain()).to.deep.equal([0, 35]); + done(); }); }); diff --git a/src/test/sources/BamDataSource-test.js b/src/test/sources/BamDataSource-test.js index 72e94383..38e7c61e 100644 --- a/src/test/sources/BamDataSource-test.js +++ b/src/test/sources/BamDataSource-test.js @@ -12,9 +12,10 @@ describe('BamDataSource', function() { function getTestSource() { // See test/data/README.md for provenance of these files. var remoteBAI = new MappedRemoteFile('/test-data/dream.synth3.bam.bai.mapped', - [[8054040, 8242920]]), - remoteBAM = new MappedRemoteFile('/test-data/dream.synth3.bam.mapped', - [[0, 69453], [163622109888, 163622739903]]); + [[8054040, 8242920]]); + + var remoteBAM = new MappedRemoteFile('/test-data/dream.synth3.bam.mapped', + [[0, 69453], [163622109888, 163622739903]]); var bam = new Bam(remoteBAM, remoteBAI, { // "chunks" is usually an array; here we take advantage of the @@ -54,16 +55,18 @@ describe('BamDataSource', function() { var source = getTestSource(); // Requests are for 'chr20', while the canonical name is just '20'. - var range = new ContigInterval('chr20', 31512050, 31512150), - rangeBefore = new ContigInterval('chr20', 31512000, 31512050), - rangeAfter = new ContigInterval('chr20', 31512150, 31512199); + var range = new ContigInterval('chr20', 31512050, 31512150); + + var rangeBefore = new ContigInterval('chr20', 31512000, 31512050); + + var rangeAfter = new ContigInterval('chr20', 31512150, 31512199); var reads = source.getAlignmentsInRange(range); expect(reads).to.deep.equal([]); var networkEvents = []; - source.on('networkprogress', event => { networkEvents.push(event) }); - source.on('networkdone', () => { networkEvents.push('networkdone') }); + source.on('networkprogress', event => { networkEvents.push(event); }); + source.on('networkdone', () => { networkEvents.push('networkdone'); }); // Fetching [50, 150] should cache [0, 200] source.on('newdata', () => { @@ -72,8 +75,9 @@ describe('BamDataSource', function() { expect(reads[0].toString()).to.equal('20:31511951-31512051'); expect(reads[17].toString()).to.equal('20:31512146-31512246'); - var readsBefore = source.getAlignmentsInRange(rangeBefore), - readsAfter = source.getAlignmentsInRange(rangeAfter); + var readsBefore = source.getAlignmentsInRange(rangeBefore); + + var readsAfter = source.getAlignmentsInRange(rangeAfter); expect(readsBefore).to.have.length(26); expect(readsAfter).to.have.length(12); @@ -103,9 +107,9 @@ describe('BamDataSource', function() { it('should only fetch new features', function(done) { var source = getTestSource(); source.once('newdata', range => { - expect(range.toString()).to.equal('20:31512100-31512400'); // expanded range + expect(range.toString()).to.equal('20:31512100-31512400'); // expanded range source.once('newdata', range => { - expect(range.toString()).to.equal('20:31512000-31512099'); // only 100bp + expect(range.toString()).to.equal('20:31512000-31512099'); // only 100bp done(); }); // This range is 100bp to the left of the previous one. diff --git a/src/test/sources/BigBedDataSource-test.js b/src/test/sources/BigBedDataSource-test.js index 0f11ea91..5d62e909 100644 --- a/src/test/sources/BigBedDataSource-test.js +++ b/src/test/sources/BigBedDataSource-test.js @@ -11,7 +11,7 @@ describe('BigBedDataSource', function() { function getTestSource() { // See test/data/README.md return BigBedDataSource.createFromBigBedFile( - new BigBed('/test-data/ensembl.chr17.bb')); + new BigBed('/test-data/ensembl.chr17.bb')); } it('should extract features in a range', function(done) { diff --git a/src/test/sources/GA4GHAlignmentSource-test.js b/src/test/sources/GA4GHAlignmentSource-test.js index c1a72d86..6f206d45 100644 --- a/src/test/sources/GA4GHAlignmentSource-test.js +++ b/src/test/sources/GA4GHAlignmentSource-test.js @@ -10,16 +10,17 @@ import GA4GHAlignmentSource from '../../main/sources/GA4GHAlignmentSource'; import RemoteFile from '../../main/RemoteFile'; describe('GA4GHAlignmentSource', function() { - var server: any = null, response; + var server: any = null; + var response: any = null; - before(function () { + before(function(): any { return new RemoteFile('/test-data/alignments.ga4gh.1.10000-11000.json').getAllString().then(data => { response = data; server = sinon.fakeServer.create(); // _after_ we do a real XHR! }); }); - after(function () { + after(function() { server.restore(); }); diff --git a/src/test/sources/GA4GHFeatureSource-test.js b/src/test/sources/GA4GHFeatureSource-test.js index d9aec209..6281d17b 100644 --- a/src/test/sources/GA4GHFeatureSource-test.js +++ b/src/test/sources/GA4GHFeatureSource-test.js @@ -12,7 +12,7 @@ import RemoteFile from '../../main/RemoteFile'; describe('GA4GHFeatureSource', function() { var server: any = null, response, source; - beforeEach(function () { + beforeEach(function(): any { source = GA4GHFeatureSource.create({ endpoint: '/v0.6.0', featureSetId: "WyIxa2dlbm9tZXMiLCJ2cyIsInBoYXNlMy1yZWxlYXNlIl0", @@ -25,7 +25,7 @@ describe('GA4GHFeatureSource', function() { }); - afterEach(function () { + afterEach(function() { server.restore(); }); diff --git a/src/test/sources/GA4GHVariantSource-test.js b/src/test/sources/GA4GHVariantSource-test.js index 59ee0273..f5d59d4d 100644 --- a/src/test/sources/GA4GHVariantSource-test.js +++ b/src/test/sources/GA4GHVariantSource-test.js @@ -12,7 +12,7 @@ import RemoteFile from '../../main/RemoteFile'; describe('GA4GHVariantSource', function() { var server: any = null, response, source; - beforeEach(function () { + beforeEach(function(): any { source = GA4GHVariantSource.create({ endpoint: '/v0.6.0', variantSetId: "WyIxa2dlbm9tZXMiLCJ2cyIsInBoYXNlMy1yZWxlYXNlIl0", @@ -26,7 +26,7 @@ describe('GA4GHVariantSource', function() { }); - afterEach(function () { + afterEach(function() { server.restore(); }); diff --git a/src/test/sources/TwoBitDataSource-test.js b/src/test/sources/TwoBitDataSource-test.js index e04b508b..fefaa7f4 100644 --- a/src/test/sources/TwoBitDataSource-test.js +++ b/src/test/sources/TwoBitDataSource-test.js @@ -9,7 +9,7 @@ import TwoBitDataSource from '../../main/sources/TwoBitDataSource'; import RemoteFile from '../../main/RemoteFile'; describe('TwoBitDataSource', function() { - function getTestSource() { + function getTestSource () { // See description of this file in TwoBit-test.js var tb = new TwoBit(new RemoteFile('/test-data/test.2bit')); return TwoBitDataSource.createFromTwoBitFile(tb); @@ -36,19 +36,19 @@ describe('TwoBitDataSource', function() { * (in millions) and afterwards we set the range to small subrange * of the huge range. The huge range shouldn't be fetched from * 2bit file. But due to //github.com/hammerlab/pileup.js/issues/416 - * every small request from the big range wasn't handled properly + * every small request from the big range wasn't handled properly * afterwardfs. - * + * */ it('should fetch base pairs (bug 416)', function(done) { var source = getTestSource(); - //this range shouldn't be fetched because is huge - var hugeRange= {contig: 'chr22', start: 0, stop: 114529884}; + // this range shouldn't be fetched because is huge + var hugeRange = {contig: 'chr22', start: 0, stop: 114529884}; - //small range that due to bug wasn't properly handled + // small range that due to bug wasn't properly handled var smallSubRange = {contig: 'chr22', start: 0, stop: 3}; source.on('newdata', () => { - //should be called only once when short chunk is requested + // should be called only once when short chunk is requested expect(source.getRange(smallSubRange)).to.deep.equal({ 'chr22:0': 'N', 'chr22:1': 'T', @@ -59,10 +59,10 @@ describe('TwoBitDataSource', function() { done(); }); - //try to fetch huge chunk of data (should be skipped) + // try to fetch huge chunk of data (should be skipped) source.rangeChanged(hugeRange); - //and now try to fetch small chunk (should be fetched and proper newdata event should be dispatched) + // and now try to fetch small chunk (should be fetched and proper newdata event should be dispatched) source.rangeChanged(smallSubRange); }); @@ -97,32 +97,34 @@ describe('TwoBitDataSource', function() { source.on('newdata', () => { expect(source.getRange({contig: 'chr22', start: 0, stop: 14})) - .to.deep.equal({ - 'chr22:0': 'N', - 'chr22:1': 'T', - 'chr22:2': 'C', - 'chr22:3': 'A', - 'chr22:4': 'C', // start of actual request - 'chr22:5': 'A', - 'chr22:6': 'G', - 'chr22:7': 'A', - 'chr22:8': 'T', - 'chr22:9': 'C', // end of actual requuest - 'chr22:10': 'A', - 'chr22:11': 'C', - 'chr22:12': 'C', - 'chr22:13': 'A', - 'chr22:14': 'T' - }); + .to.deep.equal({ + 'chr22:0': 'N', + 'chr22:1': 'T', + 'chr22:2': 'C', + 'chr22:3': 'A', + 'chr22:4': 'C', // start of actual request + 'chr22:5': 'A', + 'chr22:6': 'G', + 'chr22:7': 'A', + 'chr22:8': 'T', + 'chr22:9': 'C', // end of actual requuest + 'chr22:10': 'A', + 'chr22:11': 'C', + 'chr22:12': 'C', + 'chr22:13': 'A', + 'chr22:14': 'T' + }); done(); }); source.rangeChanged({contig: 'chr22', start: 4, stop: 9}); }); it('should not fetch data twice', function(done) { - var file = new RemoteFile('/test-data/test.2bit'), - tb = new TwoBit(file), - source = TwoBitDataSource.createFromTwoBitFile(tb); + var file = new RemoteFile('/test-data/test.2bit'); + + var tb = new TwoBit(file); + + var source = TwoBitDataSource.createFromTwoBitFile(tb); // pre-load headers & the data. tb.getFeaturesInRange('chr22', 5, 10).then(function() { @@ -135,8 +137,8 @@ describe('TwoBitDataSource', function() { // do the same request again. source.rangeChanged({contig: 'chr22', start: 5, stop: 10}); - Q.delay(100 /*ms*/).then(function() { - expect(newDataCount).to.equal(1); // no new requests + Q.delay(100 /* ms */).then(function() { + expect(newDataCount).to.equal(1); // no new requests done(); }).done(); }); @@ -163,8 +165,9 @@ describe('TwoBitDataSource', function() { it('should allow a mix of chr and non-chr', function(done) { var source = getTestSource(); - var chrRange = {contig: 'chr22', start: 0, stop: 3}, - range = {contig: '22', start: 0, stop: 3}; + var chrRange = {contig: 'chr22', start: 0, stop: 3}; + + var range = {contig: '22', start: 0, stop: 3}; source.on('newdata', () => { expect(source.getRange(range)).to.deep.equal({ @@ -181,11 +184,12 @@ describe('TwoBitDataSource', function() { it('should only report newly-fetched ranges', function(done) { TwoBitDataSource.testBasePairsToFetch(10); - var initRange = {contig: 'chr22', start: 5, stop: 8}, - secondRange = {contig: 'chr22', start: 8, stop: 15}; + var initRange = {contig: 'chr22', start: 5, stop: 8}; + + var secondRange = {contig: 'chr22', start: 8, stop: 15}; var source = getTestSource(); source.once('newdata', newRange => { - expect(newRange.toString()).to.equal('chr22:0-10'); // expanded range + expect(newRange.toString()).to.equal('chr22:0-10'); // expanded range source.once('newdata', newRange => { // This expanded range excludes previously-fetched data. diff --git a/src/test/utils-test.js b/src/test/utils-test.js index 37e7af36..e4063562 100644 --- a/src/test/utils-test.js +++ b/src/test/utils-test.js @@ -13,22 +13,24 @@ describe('utils', function() { describe('tupleLessOrEqual', function() { var lessEqual = utils.tupleLessOrEqual; - it('should work on 1-tuples', function() { + it('should work on 1-tuples', function(done) { expect(lessEqual([0], [1])).to.be.true; expect(lessEqual([1], [0])).to.be.false; expect(lessEqual([0], [0])).to.be.true; + done(); }); - it('should work on 2-tuples', function() { + it('should work on 2-tuples', function(done) { expect(lessEqual([0, 1], [0, 2])).to.be.true; expect(lessEqual([0, 1], [0, 0])).to.be.false; expect(lessEqual([0, 1], [1, 0])).to.be.true; + done(); }); }); describe('tupleRangeOverlaps', function() { var overlap = utils.tupleRangeOverlaps; - it('should work on 1-tuples', function() { + it('should work on 1-tuples', function(done) { var ivs = [ [[0], [10]], [[5], [15]], @@ -44,9 +46,10 @@ describe('utils', function() { expect(overlap(ivs[1], empty)).to.be.false; expect(overlap(ivs[2], empty)).to.be.false; expect(overlap(ivs[3], empty)).to.be.false; + done(); }); - it('should work on 2-tuples', function() { + it('should work on 2-tuples', function(done) { expect(overlap([[0, 0], [0, 10]], [[0, 5], [0, 15]])).to.be.true; expect(overlap([[0, 0], [0, 10]], @@ -63,10 +66,11 @@ describe('utils', function() { [[-1, 10], [2, 1]])).to.be.true; expect(overlap([[3, 0], [3, 10]], [[-1, 10], [2, 1]])).to.be.false; + done(); }); }); - it('should concatenate ArrayBuffers', function() { + it('should concatenate ArrayBuffers', function(done) { var u8a = new Uint8Array([0, 1, 2, 3]), u8b = new Uint8Array([4, 5, 6]), concat = new Uint8Array(utils.concatArrayBuffers([u8a.buffer, u8b.buffer])); @@ -75,13 +79,14 @@ describe('utils', function() { result.push(concat[i]); } expect(result).to.deep.equal([0, 1, 2, 3, 4, 5, 6]); + done(); }); function bufferToText(buf) { return new jBinary(buf).read('string'); } - it('should inflate concatenated buffers', function() { + it('should inflate concatenated buffers', function(done) { var str1 = 'Hello World', str2 = 'Goodbye, World', buf1 = pako.deflate(str1), @@ -112,13 +117,15 @@ describe('utils', function() { inflated = utils.inflateConcatenatedGzip(merged, 0); expect(inflated).to.have.length(1); expect(bufferToText(inflated[0].buffer)).to.equal('Hello World'); + done(); }); - it('should add or remove chr from contig names', function() { + it('should add or remove chr from contig names', function(done) { expect(utils.altContigName('21')).to.equal('chr21'); expect(utils.altContigName('chr21')).to.equal('21'); expect(utils.altContigName('M')).to.equal('chrM'); expect(utils.altContigName('chrM')).to.equal('M'); + done(); }); describe('scaleRanges', function() { @@ -127,7 +134,7 @@ describe('utils', function() { return Math.floor((iv.stop + iv.start) / 2); } - it('should scaleRanges', function() { + it('should scaleRanges', function(done) { // Zooming in and out should not change the center. // See https://github.com/hammerlab/pileup.js/issues/321 var iv = new Interval(7, 17); @@ -145,9 +152,10 @@ describe('utils', function() { iv4 = utils.scaleRange(iv4, 2.0); } expect(iv4.toString()).to.equal(iv3.toString()); + done(); }); - it('should preserve centers', function() { + it('should preserve centers', function(done) { function checkCenterThroughZoom(origIv: Interval) { var c = center(origIv); // Zoom in then out @@ -166,61 +174,70 @@ describe('utils', function() { checkCenterThroughZoom(new Interval(8, 18)); checkCenterThroughZoom(new Interval(8, 19)); checkCenterThroughZoom(new Interval(7, 18)); + done(); }); - it('should stay positive', function() { + it('should stay positive', function(done) { var iv = new Interval(5, 25), iv2 = utils.scaleRange(iv, 2.0); expect(iv2.toString()).to.equal('[0, 40]'); + done(); }); }); describe('formatInterval', function() { - it('should add commas to numbers', function() { + it('should add commas to numbers', function(done) { expect(utils.formatInterval(new Interval(0, 1234))).to.equal('0-1,234'); expect(utils.formatInterval(new Interval(1234, 567890123))).to.equal('1,234-567,890,123'); + done(); }); }); describe('parseRange', function() { var parseRange = utils.parseRange; - it('should parse intervals with and without commas', function() { + it('should parse intervals with and without commas', function(done) { expect(parseRange('1-1234')).to.deep.equal({start: 1, stop: 1234}); expect(parseRange('1-1,234')).to.deep.equal({start: 1, stop: 1234}); expect(parseRange('1-1,234')).to.deep.equal({start: 1, stop: 1234}); expect(parseRange('1,234-567,890,123')).to.deep.equal({start:1234, stop:567890123}); + done(); }); - it('should parse bare contigs', function() { + it('should parse bare contigs', function(done) { expect(parseRange('17:')).to.deep.equal({contig: '17'}); expect(parseRange('chr17')).to.deep.equal({contig: 'chr17'}); expect(parseRange('17')).to.deep.equal({start: 17}); // this one is ambiguous + done(); }); - it('should parse contig + location', function() { + it('should parse contig + location', function(done) { expect(parseRange('17:1,234')).to.deep.equal({contig: '17', start: 1234}); expect(parseRange('chrM:1,234,567')).to.deep.equal({contig: 'chrM', start: 1234567}); + done(); }); - it('should parse combined locations', function() { + it('should parse combined locations', function(done) { expect(parseRange('17:1,234-5,678')).to.deep.equal( {contig: '17', start: 1234, stop: 5678}); + done(); }); - it('should return null for invalid ranges', function() { + it('should return null for invalid ranges', function(done) { expect(parseRange('::')).to.be.null; + done(); }); }); - it('should flatMap', function() { + it('should flatMap', function(done) { expect(utils.flatMap([1, 2, 3], x => [x])).to.deep.equal([1, 2, 3]); expect(utils.flatMap([1, 2, 3], x => x % 2 === 0 ? [x, x] : [])).to.deep.equal([2, 2]); expect(utils.flatMap([[1,2], [2,3]], a => a)).to.deep.equal([1, 2, 2, 3]); expect(utils.flatMap([[1,2], [2,3]], a => [a])).to.deep.equal([[1, 2], [2, 3]]); + done(); }); - it('should compute percentiles', function() { + it('should compute percentiles', function(done) { // 75 50 25 var xs = [7, 6, 5, 4, 3, 2, 1]; expect(utils.computePercentile(xs, 50)).to.equal(4); // median @@ -233,6 +250,7 @@ describe('utils', function() { // pathological cases expect(utils.computePercentile([1], 99)).to.equal(1); expect(utils.computePercentile([], 99)).to.equal(0); + done(); }); }); diff --git a/src/test/viz/CoverageCache-test.js b/src/test/viz/CoverageCache-test.js index 8269ad04..74652096 100644 --- a/src/test/viz/CoverageCache-test.js +++ b/src/test/viz/CoverageCache-test.js @@ -3,8 +3,6 @@ */ 'use strict'; -import type {Alignment, CigarOp, MateProperties, Strand} from '../../main/Alignment'; - import {expect} from 'chai'; import _ from 'underscore'; @@ -17,13 +15,13 @@ describe('CoverageCache', function() { return new ContigInterval(chr, start, end); } - function makeCache(args) { + function makeCache(args: any) { var cache = new CoverageCache(fakeSource); _.flatten(args).forEach(read => cache.addAlignment(read)); return cache; } - it('should collect coverage', function() { + it('should collect coverage', function(done) { var cache = makeCache([ makeReadPair(ci('chr1', 100, 200), ci('chr1', 800, 900)), makeReadPair(ci('chr1', 300, 400), ci('chr1', 750, 850)), @@ -37,11 +35,12 @@ describe('CoverageCache', function() { expect(bins[850]).to.deep.equal({count: 2}); expect(bins[851]).to.deep.equal({count: 1}); expect(cache.maxCoverageForRef('chr1')).to.equal(2); + done(); }); - it('should collect mismatches', function() { + it('should collect mismatches', function(done) { var letter = '.'; // pretend the reference is this letter, repeated - var refSource = _.extend({}, fakeSource, { + var refSource = _.extend(_.clone(fakeSource), { getRangeAsString: function(range) { return letter.repeat(range.stop - range.start + 1); } @@ -91,5 +90,6 @@ describe('CoverageCache', function() { expect(bins[15]).to.deep.equal({count: 6, ref: 'C', mismatches: {A: 4, T: 1, G: 1}}); expect(bins[17]).to.deep.equal({count: 4, ref: 'C', mismatches: {A: 2}}); expect(cache.maxCoverageForRef('chr1')).to.equal(6); + done(); }); }); diff --git a/src/test/viz/CoverageTrack-test.js b/src/test/viz/CoverageTrack-test.js index 17537fc1..57d58fd3 100644 --- a/src/test/viz/CoverageTrack-test.js +++ b/src/test/viz/CoverageTrack-test.js @@ -6,8 +6,6 @@ */ 'use strict'; -import type SamRead from '../../main/data/SamRead'; - import {expect} from 'chai'; import pileup from '../../main/pileup'; @@ -19,6 +17,7 @@ import {waitFor} from '../async'; describe('CoverageTrack', function() { var testDiv = document.getElementById('testdiv'); + if (!testDiv) throw new Error("Failed to match: testdiv"); var range = {contig: '17', start: 7500730, stop: 7500790}; var p; @@ -74,22 +73,22 @@ describe('CoverageTrack', function() { return drawnObjectsWith(testDiv, '.coverage', l => l.type == 'label'); }; - var hasCoverage = () => { + var hasCoverage = (): boolean => { // Check whether the coverage bins are loaded yet - return testDiv.querySelector('canvas') && + return testDiv.querySelector('canvas') != null && findCoverageBins().length > 1 && findMismatchBins().length > 0 && findCoverageLabels().length > 1; }; - it('should create coverage information for all bases shown in the view', function() { + it('should create coverage information for all bases shown in the view', function(): any { return waitFor(hasCoverage, 2000).then(() => { var bins = findCoverageBins(); expect(bins).to.have.length.at.least(range.stop - range.start + 1); }); }); - it('should show mismatch information', function() { + it('should show mismatch information', function(): any { return waitFor(hasCoverage, 2000).then(() => { var visibleMismatches = findMismatchBins() .filter(bin => bin.position >= range.start && bin.position <= range.stop); @@ -101,7 +100,7 @@ describe('CoverageTrack', function() { }); }); - it('should create correct labels for coverage', function() { + it('should create correct labels for coverage', function(): any { return waitFor(hasCoverage, 2000).then(() => { // These are the objects being used to draw labels var labelTexts = findCoverageLabels(); diff --git a/src/test/viz/FeatureTrack-test.js b/src/test/viz/FeatureTrack-test.js index 20d3d38a..f8a5df9a 100644 --- a/src/test/viz/FeatureTrack-test.js +++ b/src/test/viz/FeatureTrack-test.js @@ -19,187 +19,223 @@ import {yForRow} from '../../main/viz/pileuputils'; import ReactTestUtils from 'react-addons-test-utils'; describe('FeatureTrack', function() { - var testDiv = document.getElementById('testdiv'); - var json; - - beforeEach(() => { - testDiv.style.width = '800px'; - dataCanvas.RecordingContext.recordAll(); - }); - - afterEach(() => { - dataCanvas.RecordingContext.reset(); - // avoid pollution between tests. - testDiv.innerHTML = ''; - }); - - before(function () { - return new RemoteFile('/test-data/features.ga4gh.chr1.120000-125000.json').getAllString().then(data => { - json = data; - }); - }); + var testDiv= document.getElementById('testdiv'); + if (!testDiv) throw new Error("Failed to match: testdiv"); var drawnObjects = dataCanvas.RecordingContext.drawnObjects; - function ready() { - return testDiv.querySelector('canvas') && - drawnObjects(testDiv, '.features').length > 0; - } + function ready(): boolean { + if (testDiv.querySelector('canvas') != null && + drawnObjects(testDiv, '.features').length > 0) { + // for flow to be happy. Need to check whether + // feature selector exists before checking its height + var featureSelector = testDiv.querySelector('.features'); + if (featureSelector != null) { + return parseInt(featureSelector.style.height) > 0; + } else { + return false; + } + } else { + return false; + } - it('should render features with json', function() { - var featureClickedData = null; - var featureClicked = function (data) { - featureClickedData = data; - }; - - var p = pileup.create(testDiv, { - range: {contig: 'chr1', start: 130000, stop: 135000}, - tracks: [ - { - viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), - isReference: true - }, - { - viz: pileup.viz.features(), - data: pileup.formats.featureJson(json), - options: {onFeatureClicked: featureClicked}, - } - ] - }); - - - return waitFor(ready, 2000) - .then(() => { - var features = drawnObjects(testDiv, '.features'); - // there can be duplicates in the case where features are - // overlapping more than one section of the canvas - features = _.uniq(features, false, function(x) { - return x.position.start(); - }); + } - expect(features).to.have.length(4); - expect(features.map(f => f.position.start())).to.deep.equal( - [89295, 92230, 110953, 120725]); + describe('jsonFeatures', function() { + var json; - var height = yForRow(4) * window.devicePixelRatio; // should be 4 rows - expect(testDiv.querySelector('.features').style.height).to.equal(`${height}px`); + beforeEach(() => { + testDiv.style.width = '800px'; + dataCanvas.RecordingContext.recordAll(); + }); - // check clicking on feature TODO - var canvasList = testDiv.getElementsByTagName('canvas'); - var canvas = canvasList[1]; - expect(featureClickedData).to.be.null; - ReactTestUtils.Simulate.click(canvas,{nativeEvent: {offsetX: 430, offsetY: 50 * window.devicePixelRatio}}); - expect(featureClickedData).to.not.be.null; + afterEach(() => { + dataCanvas.RecordingContext.reset(); + // avoid pollution between tests. + testDiv.innerHTML = ''; + }); - p.destroy(); + before(function(): any { + return new RemoteFile('/test-data/features.ga4gh.chr1.120000-125000.json').getAllString().then(data => { + json = data; }); - }); - - it('should render features with bigBed file', function() { - var featureClickedData = null; - var featureClicked = function (data) { - featureClickedData = data; - }; - - var p = pileup.create(testDiv, { - range: {contig: 'chr17', start: 10000, stop: 16500}, - tracks: [ - { - viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), - isReference: true - }, - { - viz: pileup.viz.features(), - data: pileup.formats.bigBed({ - url: '/test-data/chr17.22.10000-21000.bb', - }), - options: {onFeatureClicked: featureClicked}, - name: 'Features' - } - ] }); + it('should render features with json', function(): any { + var featureClickedData = null; + var featureClicked = function(data) { + featureClickedData = data; + }; + + var p = pileup.create(testDiv, { + range: {contig: 'chr1', start: 130000, stop: 135000}, + tracks: [ + { + viz: pileup.viz.genome(), + data: pileup.formats.twoBit({ + url: '/test-data/test.2bit' + }), + isReference: true + }, + { + viz: pileup.viz.features(), + data: pileup.formats.featureJson(json), + options: {onFeatureClicked: featureClicked}, + } + ] + }); - return waitFor(ready, 2000) - .then(() => { - var features = drawnObjects(testDiv, '.features'); - // there can be duplicates in the case where features are - // overlapping more than one section of the canvas - features = _.uniq(features, false, function(x) { - return x.position.start(); + return waitFor(ready, 2000) + .then(() => { + var features = drawnObjects(testDiv, '.features'); + // there can be duplicates in the case where features are + // overlapping more than one section of the canvas + features = _.uniq(features, false, function(x) { + return x.position.start(); + }); + + expect(features).to.have.length(4); + expect(features.map(f => f.position.start())).to.deep.equal( + [89295, 92230, 110953, 120725]); + + var height = yForRow(4) * window.devicePixelRatio; // should be 4 rows + features = testDiv.querySelector('.features'); + expect(features).to.not.be.null; + if (features != null) { + expect(features.style.height).to.equal(`${height}px`); + } + + // check clicking on feature TODO + var canvasList = testDiv.getElementsByTagName('canvas'); + var canvas = canvasList[1]; + expect(featureClickedData).to.be.null; + ReactTestUtils.Simulate.click(canvas,{nativeEvent: {offsetX: 430, offsetY: 50 * window.devicePixelRatio}}); + expect(featureClickedData).to.not.be.null; + + p.destroy(); }); + }); + }); - expect(features).to.have.length(5); - expect(features.map(f => f.position.start())).to.deep.equal( - [10000, 10150, 10400, 16000, 16180]); - - // canvas height should be height of features that are overlapping - var height = yForRow(2) * window.devicePixelRatio; // should be 2 rows - expect(testDiv.querySelector('.features').style.height).to.equal(`${height}px`); - - // check clicking on feature in row 0 - var canvasList = testDiv.getElementsByTagName('canvas'); - var canvas = canvasList[1]; - expect(featureClickedData).to.be.null; - ReactTestUtils.Simulate.click(canvas,{nativeEvent: {offsetX: 0, offsetY: 10}}); - expect(featureClickedData).to.not.be.null; + describe('bigBedFeatures', function() { - // check clicking on feature in row 1 - featureClickedData = null; - ReactTestUtils.Simulate.click(canvas,{nativeEvent: {offsetX: 55, offsetY: 10}}); - expect(featureClickedData).to.not.be.null; + beforeEach(() => { + testDiv.style.width = '800px'; + dataCanvas.RecordingContext.recordAll(); + }); - p.destroy(); + afterEach(() => { + dataCanvas.RecordingContext.reset(); + // avoid pollution between tests. + testDiv.innerHTML = ''; + }); + it('should render features with bigBed file', function(): any { + var featureClickedData = null; + var featureClicked = function(data) { + featureClickedData = data; + }; + + var p = pileup.create(testDiv, { + range: {contig: 'chr17', start: 10000, stop: 16500}, + tracks: [ + { + viz: pileup.viz.genome(), + data: pileup.formats.twoBit({ + url: '/test-data/test.2bit' + }), + isReference: true + }, + { + viz: pileup.viz.features(), + data: pileup.formats.bigBed({ + url: '/test-data/chr17.22.10000-21000.bb', + }), + options: {onFeatureClicked: featureClicked}, + name: 'Features' + } + ] }); - }); - it('should not exceed parent height limits', function() { - - var p = pileup.create(testDiv, { - range: {contig: 'chr22', start: 20000, stop: 21000}, - tracks: [ - { - viz: pileup.viz.genome(), - data: pileup.formats.twoBit({ - url: '/test-data/test.2bit' - }), - isReference: true - }, - { - viz: pileup.viz.features(), - data: pileup.formats.bigBed({ - url: '/test-data/chr17.22.10000-21000.bb', - }), - name: 'Features' - } - ] - }); - return waitFor(ready, 2000) - .then(() => { - var features = drawnObjects(testDiv, '.features'); - // there can be duplicates in the case where features are - // overlapping more than one section of the canvas - features = _.uniq(features, false, function(x) { - return x.position.start(); + return waitFor(ready, 2000) + .then(() => { + var features = drawnObjects(testDiv, '.features'); + // there can be duplicates in the case where features are + // overlapping more than one section of the canvas + features = _.uniq(features, false, function(x) { + return x.position.start(); + }); + + expect(features).to.have.length(5); + expect(features.map(f => f.position.start())).to.deep.equal( + [10000, 10150, 10400, 16000, 16180]); + + // canvas height should be height of features that are overlapping + var height = yForRow(2) * window.devicePixelRatio; // should be 2 rows + + features = testDiv.querySelector('.features'); + expect(features).to.not.be.null; + if (features != null) { + expect(features.style.height).to.equal(`${height}px`); + } + // check clicking on feature in row 0 + var canvasList = testDiv.getElementsByTagName('canvas'); + var canvas = canvasList[1]; + expect(featureClickedData).to.be.null; + ReactTestUtils.Simulate.click(canvas,{nativeEvent: {offsetX: 0, offsetY: 10}}); + expect(featureClickedData).to.not.be.null; + + // check clicking on feature in row 1 + featureClickedData = null; + ReactTestUtils.Simulate.click(canvas,{nativeEvent: {offsetX: 55, offsetY: 10}}); + expect(featureClickedData).to.not.be.null; + + p.destroy(); }); + }); - expect(features).to.have.length(10); - - // canvas height should be maxed out - var expectedHeight = 150 * window.devicePixelRatio; - expect(testDiv.querySelector('.features').style.height).to.equal(`${expectedHeight}px`); - - p.destroy(); - + it('should not exceed parent height limits', function(): any { + var p = pileup.create(testDiv, { + range: {contig: 'chr22', start: 20000, stop: 21000}, + tracks: [ + { + viz: pileup.viz.genome(), + data: pileup.formats.twoBit({ + url: '/test-data/test.2bit' + }), + isReference: true + }, + { + viz: pileup.viz.features(), + data: pileup.formats.bigBed({ + url: '/test-data/chr17.22.10000-21000.bb', + }), + name: 'Features' + } + ] }); - }); + return waitFor(ready, 2000) + .then(() => { + var features = drawnObjects(testDiv, '.features'); + // there can be duplicates in the case where features are + // overlapping more than one section of the canvas + features = _.uniq(features, false, function(x) { + return x.position.start(); + }); + + expect(features).to.have.length(10); + + // canvas height should be maxed out + var expectedHeight = 150 * window.devicePixelRatio; + var featureCanvas = testDiv.querySelector('.features'); + expect(featureCanvas).to.not.be.null; + if (featureCanvas != null) { + expect(featureCanvas.style.height).to.equal(`${expectedHeight}px`); + } + p.destroy(); + }); + }); + }); }); diff --git a/src/test/viz/GeneTrack-test.js b/src/test/viz/GeneTrack-test.js index 2ba13b02..772e2c77 100644 --- a/src/test/viz/GeneTrack-test.js +++ b/src/test/viz/GeneTrack-test.js @@ -15,7 +15,8 @@ import {waitFor} from '../async'; describe('GeneTrack', function() { var testDiv = document.getElementById('testdiv'); - + if (!testDiv) throw new Error("Failed to match: testdiv"); + beforeEach(() => { testDiv.style.width = '800px'; dataCanvas.RecordingContext.recordAll(); @@ -28,12 +29,12 @@ describe('GeneTrack', function() { }); var {drawnObjects, callsOf} = dataCanvas.RecordingContext; - function ready() { - return testDiv.querySelector('canvas') && + function ready(): boolean { + return testDiv.querySelector('canvas') != null && drawnObjects(testDiv, '.genes').length > 0; } - it('should render genes', function() { + it('should render genes', function(): any { var p = pileup.create(testDiv, { range: {contig: '17', start: 9386380, stop: 9537390}, tracks: [ diff --git a/src/test/viz/GenericFeatureCache-test.js b/src/test/viz/GenericFeatureCache-test.js index 0183ca64..8c12911d 100644 --- a/src/test/viz/GenericFeatureCache-test.js +++ b/src/test/viz/GenericFeatureCache-test.js @@ -3,8 +3,6 @@ */ 'use strict'; -import type {Alignment, CigarOp, MateProperties, Strand} from '../../main/Alignment'; - import {expect} from 'chai'; import _ from 'underscore'; @@ -18,13 +16,13 @@ describe('GenericFeatureCache', function() { return new ContigInterval(chr, start, end); } - function makeCache(features) { + function makeCache(features: any) { var cache = new GenericFeatureCache(fakeSource); _.flatten(features).forEach(feature => cache.addFeature(feature)); return cache; } - it('should put non-overlapping features in the same row', function() { + it('should put non-overlapping features in the same row', function(done) { var a = {id: "A", featureType: "Feature", position: new ContigInterval('chr1', 100, 200), @@ -48,10 +46,11 @@ describe('GenericFeatureCache', function() { expect(groups[1].row).to.equal(0); expect(groups[2].row).to.equal(0); expect(cache.pileupHeightForRef('chr1')).to.equal(1); + done(); }); - it('should put overlapping features in the different row', function() { + it('should put overlapping features in the different row', function(done) { var a = {id: "A", featureType: "Feature", position: new ContigInterval('chr1', 100, 200), @@ -75,9 +74,10 @@ describe('GenericFeatureCache', function() { expect(groups[1].row).to.equal(1); expect(groups[2].row).to.equal(2); expect(cache.pileupHeightForRef('chr1')).to.equal(3); + done(); }); - it('should find overlapping features', function() { + it('should find overlapping features', function(done) { var a = {id: "A", featureType: "Feature", position: new ContigInterval('chr1', 100, 200), @@ -101,6 +101,7 @@ describe('GenericFeatureCache', function() { // 'chr'-tolerance expect(cache.getGroupsOverlapping(ci('chr1', 100, 200))).to.have.length(1); + done(); }); }); diff --git a/src/test/viz/GenomeTrack-test.js b/src/test/viz/GenomeTrack-test.js index 0af362b7..8e7427af 100644 --- a/src/test/viz/GenomeTrack-test.js +++ b/src/test/viz/GenomeTrack-test.js @@ -20,7 +20,8 @@ import {waitFor} from '../async'; describe('GenomeTrack', function() { var testDiv = document.getElementById('testdiv'); - + if (!testDiv) throw new Error("Failed to match: testdiv"); + beforeEach(() => { // A fixed width container results in predictable x-positions for mismatches. testDiv.style.width = '800px'; @@ -40,19 +41,19 @@ describe('GenomeTrack', function() { referenceSource = TwoBitDataSource.createFromTwoBitFile(new TwoBit(twoBitFile)); var {drawnObjects} = dataCanvas.RecordingContext; - var hasReference = () => { + var hasReference = (): boolean => { // The reference initially shows "unknown" base pairs, so we have to // check for a specific known one to ensure that it's really loaded. - return testDiv.querySelector('canvas') && + return testDiv.querySelector('canvas') != null && drawnObjects(testDiv, '.reference').length > 0; }; - var referenceTrackLoaded = () => { + var referenceTrackLoaded = (): boolean => { //this can be done in a preatier way return testDiv.querySelector('canvas') !== null ; }; - it('should tolerate non-chr ranges', function() { + it('should tolerate non-chr ranges', function(): any { var p = pileup.create(testDiv, { range: {contig: '17', start: 7500730, stop: 7500790}, tracks: [ @@ -92,7 +93,7 @@ describe('GenomeTrack', function() { * (range span is milions of nucleotides) into very narrow view * (tens of nucleotides). */ - it('should zoom from huge zoom out', function() { + it('should zoom from huge zoom out', function(): any { var p = pileup.create(testDiv, { range: { contig: '17', start: 0, stop: 114529884 }, @@ -125,7 +126,7 @@ describe('GenomeTrack', function() { }); }); - it('should zoom in and out', function() { + it('should zoom in and out', function(): any { var p = pileup.create(testDiv, { range: {contig: '17', start: 7500725, stop: 7500775}, tracks: [ @@ -168,7 +169,7 @@ describe('GenomeTrack', function() { }); }); - it('should accept user-entered locations', function() { + it('should accept user-entered locations', function(): any { var p = pileup.create(testDiv, { range: {contig: '17', start: 7500725, stop: 7500775}, tracks: [ diff --git a/src/test/viz/PileupCache-test.js b/src/test/viz/PileupCache-test.js index 9a5ec710..cc00796f 100644 --- a/src/test/viz/PileupCache-test.js +++ b/src/test/viz/PileupCache-test.js @@ -3,8 +3,6 @@ */ 'use strict'; -import type {Alignment, CigarOp, MateProperties, Strand} from '../../main/Alignment'; - import {expect} from 'chai'; import _ from 'underscore'; @@ -20,13 +18,13 @@ describe('PileupCache', function() { return new ContigInterval(chr, start, end); } - function makeCache(args, viewAsPairs: boolean) { + function makeCache(args: any, viewAsPairs: boolean) { var cache = new PileupCache(fakeSource, viewAsPairs); _.flatten(args).forEach(read => cache.addAlignment(read)); return cache; } - it('should group read pairs', function() { + it('should group read pairs', function(done) { var cache = makeCache(makeReadPair(ci('chr1', 100, 200), ci('chr1', 300, 400)), true /* viewAsPairs */); @@ -43,9 +41,10 @@ describe('PileupCache', function() { expect(g.span.toString()).to.equal('chr1:100-400'); expect(cache.pileupHeightForRef('chr1')).to.equal(1); expect(cache.pileupHeightForRef('chr2')).to.equal(0); + done(); }); - it('should group pile up pairs', function() { + it('should group pile up pairs', function(done) { // A & B overlap, B & C overlap, but A & C do not. So two rows will suffice. var cache = makeCache([ makeReadPair(ci('chr1', 100, 200), ci('chr1', 300, 400)), // A @@ -59,9 +58,10 @@ describe('PileupCache', function() { expect(groups[1].row).to.equal(1); expect(groups[2].row).to.equal(0); expect(cache.pileupHeightForRef('chr1')).to.equal(2); + done(); }); - it('should pile pairs which overlap only in their inserts', function() { + it('should pile pairs which overlap only in their inserts', function(done) { // No individual reads overlap, but they do when their inserts are included. var cache = makeCache([ makeReadPair(ci('chr1', 100, 200), ci('chr1', 800, 900)), @@ -73,9 +73,10 @@ describe('PileupCache', function() { expect(groups[0].row).to.equal(0); expect(groups[1].row).to.equal(1); expect(cache.pileupHeightForRef('chr1')).to.equal(2); + done(); }); - it('should pack unpaired reads more tightly', function() { + it('should pack unpaired reads more tightly', function(done) { // Same as the previous test, but with viewAsPairs = false. // When the inserts aren't rendered, the reads all fit on a single line. var cache = makeCache([ @@ -85,9 +86,10 @@ describe('PileupCache', function() { var groups = _.values(cache.groups); expect(groups).to.have.length(4); expect(cache.pileupHeightForRef('chr1')).to.equal(1); + done(); }); - it('should separate pairs on differing contigs', function() { + it('should separate pairs on differing contigs', function(done) { var cache = makeCache(makeReadPair(ci('chr1', 100, 200), ci('chr2', 150, 250)), true /* viewAsPairs */); @@ -103,9 +105,10 @@ describe('PileupCache', function() { expect(cache.pileupHeightForRef('chr2')).to.equal(1); expect(cache.pileupHeightForRef('1')).to.equal(1); expect(cache.pileupHeightForRef('2')).to.equal(1); + done(); }); - it('should find overlapping reads', function() { + it('should find overlapping reads', function(done) { var cache = makeCache([ makeReadPair(ci('chr1', 100, 200), ci('chr1', 800, 900)), makeReadPair(ci('chr1', 300, 400), ci('chr1', 500, 600)), @@ -124,9 +127,10 @@ describe('PileupCache', function() { // 'chr'-tolerance expect(cache.getGroupsOverlapping(ci('1', 50, 150))).to.have.length(1); expect(cache.getGroupsOverlapping(ci('1', 50, 350))).to.have.length(2); + done(); }); - it('should sort reads at a locus', function() { + it('should sort reads at a locus', function(done) { var read = (start, stop) => makeRead(ci('chr1', start, stop), '+'); var cache = makeCache([ read(100, 200), @@ -180,9 +184,10 @@ describe('PileupCache', function() { [1, 'chr2:50-150'], [2, 'chr2:100-200'] ]); + done(); }); - it('should sort paired reads at a locus', function() { + it('should sort paired reads at a locus', function(done) { var cache = makeCache([ makeReadPair(ci('chr1', 100, 200), ci('chr1', 800, 900)), makeReadPair(ci('chr1', 300, 400), ci('chr1', 500, 600)) @@ -203,9 +208,10 @@ describe('PileupCache', function() { cache.sortReadsAt('chr1', 850); expect(rows()).to.deep.equal([0, 1]); + done(); }); - it('should sort a larger pileup of pairs', function() { + it('should sort a larger pileup of pairs', function(done) { // A: <--- ---> // B: <--- ---> // C: <--- ---> @@ -223,9 +229,10 @@ describe('PileupCache', function() { cache.sortReadsAt('chr1', 325); // x expect(rows()).to.deep.equal([2, 1, 0]); + done(); }); - it('should compute statistics on a BAM file', function() { + it('should compute statistics on a BAM file', function(done): any { this.timeout(5000); var bam = new Bam( new RemoteFile('/test-data/synth4.tumor.1.4930000-4950000.bam'), @@ -236,6 +243,7 @@ describe('PileupCache', function() { var stats = cache.getInsertStats(); expect(stats.minOutlierSize).to.be.within(1, 100); expect(stats.maxOutlierSize).to.be.within(500, 600); + done(); }); }); diff --git a/src/test/viz/PileupTrack-test.js b/src/test/viz/PileupTrack-test.js index 6f8c5daf..3d51d842 100644 --- a/src/test/viz/PileupTrack-test.js +++ b/src/test/viz/PileupTrack-test.js @@ -56,7 +56,7 @@ class FakeBam extends Bam { this.deferred = Q.defer(); } - getAlignmentsInRange(range: ContigInterval, opt_contained?: boolean): Q.Promise { + getAlignmentsInRange(): Q.Promise { return this.deferred.promise; } @@ -67,7 +67,8 @@ class FakeBam extends Bam { describe('PileupTrack', function() { - var testDiv = document.getElementById('testdiv'); + var testDiv = document.getElementById('testdiv'); + if (!testDiv) throw new Error("Failed to match: testdiv"); beforeEach(() => { // A fixed width container results in predictable x-positions for mismatches. @@ -89,10 +90,10 @@ describe('PileupTrack', function() { bamIndexFile = new RemoteFile('/test-data/synth3.normal.17.7500000-7515000.bam.bai'); // It simplifies the tests to have these variables available synchronously. - var reference = '', + var reference: string = '', alignments = []; - before(function() { + before(function(): any { var twoBit = new TwoBit(twoBitFile), bam = new Bam(bamFile, bamIndexFile); return twoBit.getFeaturesInRange('chr17', 7500000, 7510000).then(seq => { @@ -134,15 +135,18 @@ describe('PileupTrack', function() { var hasReference = () => { // The reference initially shows "unknown" base pairs, so we have to // check for a specific known one to ensure that it's really loaded. - return testDiv.querySelector('.reference canvas') && + return testDiv.querySelector('.reference canvas') !== null && + testDiv.querySelector('.reference canvas') !== undefined && drawnObjectsWith(testDiv, '.reference', x => x.letter).length > 0; }, hasAlignments = () => { - return testDiv.querySelector('.pileup canvas') && + return testDiv.querySelector('.pileup canvas') !== null && + testDiv.querySelector('.pileup canvas') !== undefined && drawnObjectsWith(testDiv, '.pileup', x => x.span).length > 0; }, hasPileupSelector = () => { - return testDiv.querySelector('.pileup canvas') !== undefined; + return testDiv.querySelector('.pileup canvas') !== null && + testDiv.querySelector('.pileup canvas') !== undefined; }, // Helpers for working with DataCanvas @@ -164,7 +168,7 @@ describe('PileupTrack', function() { expect(mismatchesAtPos(7500764 - 1).length).to.equal(0); }; - it('should indicate mismatches when the reference loads first', function() { + it('should indicate mismatches when the reference loads first', function(): any { var {p, fakeTwoBit, fakeBam} = testSetup(); // Release the reference first. @@ -185,7 +189,7 @@ describe('PileupTrack', function() { }); // Same as the previous test, but with the loads reversed. - it('should indicate mismatches when the alignments load first', function() { + it('should indicate mismatches when the alignments load first', function(): any { var {p, fakeTwoBit, fakeBam} = testSetup(); // Release the alignments first. @@ -203,7 +207,7 @@ describe('PileupTrack', function() { }); }); - it('should hide alignments', function() { + it('should hide alignments', function(): any { var p = pileup.create(testDiv, { range: {contig: 'chr17', start: 7500734, stop: 7500796}, tracks: [ @@ -233,7 +237,7 @@ describe('PileupTrack', function() { }); }); - it('should sort reads', function() { + it('should sort reads', function(): any { var p = pileup.create(testDiv, { range: {contig: 'chr17', start: 7500734, stop: 7500796}, tracks: [ diff --git a/src/test/viz/TiledCanvas-test.js b/src/test/viz/TiledCanvas-test.js index e508e2a5..5dd28dab 100644 --- a/src/test/viz/TiledCanvas-test.js +++ b/src/test/viz/TiledCanvas-test.js @@ -13,7 +13,7 @@ describe('TiledCanvas', function() { var getNewTileRanges = TiledCanvas.getNewTileRanges; var iv = (a, b) => new Interval(a, b); - it('should tile new ranges', function() { + it('should tile new ranges', function(done) { // The 0-100bp range gets enlarged & covered by a 0-500bp buffer. expect(getNewTileRanges([], iv(0, 100), 1)) .to.deep.equal([iv(0, 499)]); @@ -30,6 +30,7 @@ describe('TiledCanvas', function() { // There's an existing tile in the middle of the new range. expect(getNewTileRanges([iv(0, 200), iv(400, 700)], iv(350, 750), 1)) .to.deep.equal([iv(201, 399), iv(701, 999)]); + done(); }); function intervalsAfterPanning(seq: Interval[], pxPerBase: number): Interval[] { @@ -41,7 +42,7 @@ describe('TiledCanvas', function() { return tiles; } - it('should generate a small number of tiles while panning', function() { + it('should generate a small number of tiles while panning', function(done) { // This simulates a sequence of views that might result from panning. // Start at [100, 500], pan to the left, then over to the right. expect(intervalsAfterPanning( @@ -57,9 +58,10 @@ describe('TiledCanvas', function() { iv(600, 1000), iv(610, 1010)], 1)) .to.deep.equal([iv(0, 499), iv(500, 999), iv(1000, 1499)]); + done(); }); - it('should not leave gaps with non-integer pixels per base', function() { + it('should not leave gaps with non-integer pixels per base', function(done) { var pxPerBase = 1100 / 181; // ~6.077 px/bp -- very awkward! expect(intervalsAfterPanning( [iv(100, 300), @@ -84,6 +86,7 @@ describe('TiledCanvas', function() { iv(664, 746), iv(747, 829) ]); + done(); }); }); }); diff --git a/src/test/viz/VariantTrack-test.js b/src/test/viz/VariantTrack-test.js index 06842461..1fbfdd5d 100644 --- a/src/test/viz/VariantTrack-test.js +++ b/src/test/viz/VariantTrack-test.js @@ -13,6 +13,7 @@ import ReactTestUtils from 'react-addons-test-utils'; describe('VariantTrack', function() { var testDiv = document.getElementById('testdiv'); + if (!testDiv) throw new Error("Failed to match: testdiv"); beforeEach(() => { testDiv.style.width = '700px'; @@ -31,9 +32,9 @@ describe('VariantTrack', function() { drawnObjects(testDiv, '.variants').length > 0; } - it('should render variants', function() { + it('should render variants', function(): any { var variantClickedData = null; - var variantClicked = function (data) { + var variantClicked = function(data) { variantClickedData = data; }; var p = pileup.create(testDiv, { diff --git a/src/test/viz/d3utils-test.js b/src/test/viz/d3utils-test.js index 9e35ffd4..e2e59b93 100644 --- a/src/test/viz/d3utils-test.js +++ b/src/test/viz/d3utils-test.js @@ -1,14 +1,14 @@ /* @flow */ 'use strict'; -var {expect} = require('chai'); +import {expect} from 'chai'; var d3utils = require('../../main/viz/d3utils'); describe('d3utils', function() { describe('formatRange', function() { var formatRange = d3utils.formatRange; - it('should format view sizes correctly', function() { + it('should format view sizes correctly', function(done) { var r = formatRange(101); expect(r.prefix).to.be.equal("101"); expect(r.unit).to.be.equal("bp"); @@ -24,30 +24,34 @@ describe('d3utils', function() { r = formatRange(10001000001); expect(r.prefix).to.be.equal("10,001"); expect(r.unit).to.be.equal("Mbp"); + done(); }); }); - describe('getTrackScale', function() { + describe('getTrackScale', function(done) { var getTrackScale = d3utils.getTrackScale; - it('should define a linear scale', function() { + it('should define a linear scale', function(done) { var scale = getTrackScale({start: 100, stop: 200}, 1000); expect(scale(100)).to.equal(0); expect(scale(201)).to.equal(1000); + done(); }); - it('should be invertible', function() { + it('should be invertible', function(done) { var scale = getTrackScale({start: 100, stop: 200}, 1000); expect(scale.invert(0)).to.equal(100); expect(scale.invert(1000)).to.equal(201); + done(); }); - it('should be clampable', function() { + it('should be clampable', function(done) { var scale = getTrackScale({start: 100, stop: 200}, 1000); scale = scale.clamp(true); expect(scale(0)).to.equal(0); expect(scale(100)).to.equal(0); expect(scale(201)).to.equal(1000); expect(scale(500)).to.equal(1000); + done(); }); }); }); diff --git a/src/test/viz/pileuputils-test.js b/src/test/viz/pileuputils-test.js index 6254a7c4..a2538c07 100644 --- a/src/test/viz/pileuputils-test.js +++ b/src/test/viz/pileuputils-test.js @@ -26,7 +26,7 @@ describe('pileuputils', function() { }); } - it('should check the guarantee', function() { + it('should check the guarantee', function(done) { var reads = [ new Interval(0, 9), new Interval(5, 14), @@ -35,9 +35,10 @@ describe('pileuputils', function() { checkGuarantee(reads, [0, 1, 2]); // ok checkGuarantee(reads, [0, 1, 0]); // ok expect(() => checkGuarantee(reads, [0, 0, 0])).to.throw(); // not ok + done(); }); - it('should pile up a collection of reads', function() { + it('should pile up a collection of reads', function(done) { var reads = [ new Interval(0, 9), new Interval(5, 14), @@ -47,9 +48,10 @@ describe('pileuputils', function() { var rows = pileup(reads); checkGuarantee(reads, rows); expect(rows).to.deep.equal([0,1,0,1]); + done(); }); - it('should pile up a deep collection of reads', function() { + it('should pile up a deep collection of reads', function(done) { var reads = [ new Interval(0, 9), new Interval(1, 10), @@ -60,9 +62,10 @@ describe('pileuputils', function() { var rows = pileup(reads); checkGuarantee(reads, rows); expect(rows).to.deep.equal([0,1,2,3,4]); + done(); }); - it('should pile up around a long read', function() { + it('should pile up around a long read', function(done) { var reads = [ new Interval(1, 9), new Interval(0, 100), @@ -73,9 +76,10 @@ describe('pileuputils', function() { var rows = pileup(reads); checkGuarantee(reads, rows); expect(rows).to.deep.equal([0,1,2,0,2]); + done(); }); - it('should build a pileup progressively', function() { + it('should build a pileup progressively', function(done) { var reads = [ new Interval(1, 9), new Interval(0, 100), @@ -87,6 +91,7 @@ describe('pileuputils', function() { var rows = reads.map(read => addToPileup(read, pileup)); checkGuarantee(reads, rows); expect(rows).to.deep.equal([0,1,2,0,2]); + done(); }); var ref = // chr17:7513000-7513500 @@ -98,7 +103,7 @@ describe('pileuputils', function() { "GTATATATATACACACACACACACACACATATATATAAATTAGCCAGGCGTGGTGGCACATGGCTGTAACC" + "TCAGCTATTCAGGGTGGCTGAGATATGAGAATCACTTGAAGCCAGGAGGCAGAGGCTGCAGGGTCGTCTGG" + "ATTT"; - it('should split reads into ops', function() { + it('should split reads into ops', function(): any { var bamFile = new RemoteFile('/test-data/synth3.normal.17.7500000-7515000.bam'), bamIndexFile = new RemoteFile('/test-data/synth3.normal.17.7500000-7515000.bam.bai'), bam = new Bam(bamFile, bamIndexFile); diff --git a/types/types.js b/types/types.js deleted file mode 100644 index 3357c71d..00000000 --- a/types/types.js +++ /dev/null @@ -1,5 +0,0 @@ -declare class GenomeRange { - contig: string; - start: number; // inclusive - stop: number; // inclusive -}