<commit_before>define( [
            'dojo/_base/declare',
            'dojo/_base/array',
            './Util'
        ],
        function( declare, array, BAMUtil ) {
//
// Binning (transliterated from SAM1.3 spec)
//

/* calculate bin given an alignment covering [beg,end) (zero-based, half-close-half-open) */
function reg2bin(beg, end)
{
    --end;
    if (beg>>14 == end>>14) return ((1<<15)-1)/7 + (beg>>14);
    if (beg>>17 == end>>17) return ((1<<12)-1)/7 + (beg>>17);
    if (beg>>20 == end>>20) return ((1<<9)-1)/7 + (beg>>20);
    if (beg>>23 == end>>23) return ((1<<6)-1)/7 + (beg>>23);
    if (beg>>26 == end>>26) return ((1<<3)-1)/7 + (beg>>26);
    return 0;
}

/* calculate the list of bins that may overlap with region [beg,end) (zero-based) */
var MAX_BIN = (((1<<18)-1)/7);
function reg2bins(beg, end)
{
    var i = 0, k, list = [];
    --end;
    list.push(0);
    for (k = 1 + (beg>>26); k <= 1 + (end>>26); ++k) list.push(k);
    for (k = 9 + (beg>>23); k <= 9 + (end>>23); ++k) list.push(k);
    for (k = 73 + (beg>>20); k <= 73 + (end>>20); ++k) list.push(k);
    for (k = 585 + (beg>>17); k <= 585 + (end>>17); ++k) list.push(k);
    for (k = 4681 + (beg>>14); k <= 4681 + (end>>14); ++k) list.push(k);
    return list;
}

var SEQRET_DECODER = ['=', 'A', 'C', 'x', 'G', 'x', 'x', 'x', 'T', 'x', 'x', 'x', 'x', 'x', 'x', 'N'];
var CIGAR_DECODER = ['M', 'I', 'D', 'N', 'S', 'H', 'P', '=', 'X', '?', '?', '?', '?', '?', '?', '?'];

var Vob = declare( null, {
    constructor: function(b, o) {
        this.block = b;
        this.offset = o;
    },
    toString: function() {
        return '' + this.block + ':' + this.offset;
    }
});

function readVob(ba, offset) {
    var block = ((ba[offset+6] & 0xff) * 0x100000000) + ((ba[offset+5] & 0xff) * 0x1000000) + ((ba[offset+4] & 0xff) * 0x10000) + ((ba[offset+3] & 0xff) * 0x100) + ((ba[offset+2] & 0xff));
    var bint = (ba[offset+1] << 8) | (ba[offset]);
    if (block == 0 && bint == 0) {
        return null;  // Should only happen in the linear index?
    } else {
        return new Vob(block, bint);
    }
}

var Chunk = declare( null, {
    constructor: function(minv, maxv) {
        this.minv = minv;
        this.maxv = maxv;
    }
});

var BamRecord = declare( null, {} );

var readInt = BAMUtil.readInt;
var readShort = BAMUtil.readInt;

var BamFile = declare( null,

/**
 * @lends JBrowse.Store.SeqFeature.BAM.File
 */
{

    /**
     * Low-level BAM file reading code.
     *
     * Adapted by Robert Buels from bam.js in the Dalliance Genome
     * Explorer which is copyright Thomas Down 2006-2010
     * @constructs
     */
    constructor: function() {},

    blocksForRange: function(refId, min, max) {
        var index = this.indices[refId];
        if (!index) {
            return [];
        }

        var intBinsL = reg2bins(min, max);
        var intBins = [];
        for (var i = 0; i < intBinsL.length; ++i) {
            intBins[intBinsL[i]] = true;
        }
        var leafChunks = [], otherChunks = [];

        var nbin = readInt(index, 0);
        var p = 4;
        for (var b = 0; b < nbin; ++b) {
            var bin = readInt(index, p);
            var nchnk = readInt(index, p+4);
    //        dlog('bin=' + bin + '; nchnk=' + nchnk);
            p += 8;
            if (intBins[bin]) {
                for (var c = 0; c < nchnk; ++c) {
                    var cs = readVob(index, p);
                    var ce = readVob(index, p + 8);
                    (bin < 4681 ? otherChunks : leafChunks).push(new Chunk(cs, ce));
                    p += 16;
                }
            } else {
                p +=  (nchnk * 16);
            }
        }
    //    dlog('leafChunks = ' + miniJSONify(leafChunks));
    //    dlog('otherChunks = ' + miniJSONify(otherChunks));

        var nintv = readInt(index, p);
        var lowest = null;
        var minLin = Math.min(min>>14, nintv - 1), maxLin = Math.min(max>>14, nintv - 1);
        for (var i = minLin; i <= maxLin; ++i) {
            var lb =  readVob(index, p + 4 + (i * 8));
            if (!lb) {
                continue;
            }
            if (!lowest || lb.block < lowest.block || lb.offset < lowest.offset) {
                lowest = lb;
            }
        }
        // dlog('Lowest LB = ' + lowest);

        var prunedOtherChunks = [];
        if (lowest != null) {
            for (var i = 0; i < otherChunks.length; ++i) {
                var chnk = otherChunks[i];
                if (chnk.maxv.block >= lowest.block && chnk.maxv.offset >= lowest.offset) {
                    prunedOtherChunks.push(chnk);
                }
            }
        }
        // dlog('prunedOtherChunks = ' + miniJSONify(prunedOtherChunks));
        otherChunks = prunedOtherChunks;

        var intChunks = [];
        for (var i = 0; i < otherChunks.length; ++i) {
            intChunks.push(otherChunks[i]);
        }
        for (var i = 0; i < leafChunks.length; ++i) {
            intChunks.push(leafChunks[i]);
        }

        intChunks.sort(function(c0, c1) {
            var dif = c0.minv.block - c1.minv.block;
            if (dif != 0) {
                return dif;
            } else {
                return c0.minv.offset - c1.minv.offset;
            }
        });
        var mergedChunks = [];
        if (intChunks.length > 0) {
            var cur = intChunks[0];
            for (var i = 1; i < intChunks.length; ++i) {
                var nc = intChunks[i];
                if (nc.minv.block == cur.maxv.block /* && nc.minv.offset == cur.maxv.offset */) { // no point splitting mid-block
                    cur = new Chunk(cur.minv, nc.maxv);
                } else {
                    mergedChunks.push(cur);
                    cur = nc;
                }
            }
            mergedChunks.push(cur);
        }
    //    dlog('mergedChunks = ' + miniJSONify(mergedChunks));

        return mergedChunks;
    },

    fetch: function(chr, min, max, callback) {
        var chrId = this.chrToIndex[chr];
        var chunks;
        if (chrId === undefined) {
            chunks = [];
        } else {
            chunks = this.blocksForRange(chrId, min, max);
            if (!chunks) {
                callback(null, 'Error in index fetch');
            }
        }

        // this.recordCache = this.recordCache || new LRUCache({
        //     fillCallback: dojo.hitch(this, '_fetchChunkRecords' )function() {

        //     })
        // });
        // console.log( chr, min, max, chunks );

        this._fetchChunkRecords( chunks, function( records ) {
            records = array.filter( records, function( record ) {
                return (!min || record.pos <= max && record.pos + record.lseq >= min)
                    && (chrId === undefined || record._refID == chrId);
            });
            callback( records );
        });

    },

    _fetchChunkRecords: function( chunks, callback ) {
        var thisB = this;
        var records = [];
        var index = 0;
        var data;

        function tramp() {
            if (index >= chunks.length) {
                return callback(records);
            } else if (!data) {
                // dlog('fetching ' + index);
                var c = chunks[index];
                var fetchMin = c.minv.block;
                var fetchMax = c.maxv.block + (1<<16); // *sigh*
                thisB.data.slice(fetchMin, fetchMax - fetchMin)
                    .fetch(function(r) {
                               data = BAMUtil.unbgzf(r, c.maxv.block - c.minv.block + 1);
                               return tramp();
                           });
                return null;
            } else {
                var ba = new Uint8Array(data);
                thisB.readBamRecords(ba, chunks[index].minv.offset, records );
                data = null;
                ++index;
                return tramp();
            }
        };
        tramp();
    },

    readBamRecords: function(ba, offset, sink ) {
        while (true) {
            var blockSize = readInt(ba, offset);
            var blockEnd = offset + blockSize + 4;
            if (blockEnd >= ba.length) {
                return sink;
            }

            var record = new BamRecord();

            var refID = readInt(ba, offset + 4);
            var pos = readInt(ba, offset + 8);

            var bmn = readInt(ba, offset + 12);
            var bin = (bmn & 0xffff0000) >> 16;
            var mq = (bmn & 0xff00) >> 8;
            var nl = bmn & 0xff;

            var flag_nc = readInt(ba, offset + 16);
            var flag = (flag_nc & 0xffff0000) >> 16;
            var nc = flag_nc & 0xffff;

            var lseq = readInt(ba, offset + 20);
            record.lseq = lseq;

            var nextRef  = readInt(ba, offset + 24);
            var nextPos = readInt(ba, offset + 28);

            var tlen = readInt(ba, offset + 32);

            var readName = '';
            for (var j = 0; j < nl-1; ++j) {
                readName += String.fromCharCode(ba[offset + 36 + j]);
            }

            var p = offset + 36 + nl;

            var cigar = '';
            for (var c = 0; c < nc; ++c) {
                var cigop = readInt(ba, p);
                cigar = cigar + (cigop>>4) + CIGAR_DECODER[cigop & 0xf];
                p += 4;
            }
            record.cigar = cigar;

            var seq = '';
            var seqBytes = (lseq + 1) >> 1;
            for (var j = 0; j < seqBytes; ++j) {
                var sb = ba[p + j];
                seq += SEQRET_DECODER[(sb & 0xf0) >> 4];
                seq += SEQRET_DECODER[(sb & 0x0f)];
            }
            p += seqBytes;
            record.seq = seq;

            var qseq = '';
            for (var j = 0; j < lseq; ++j) {
                qseq += String.fromCharCode(ba[p + j]);
            }
            p += lseq;
            record.quals = qseq;

            record.pos = pos;
            record.MQ = mq;
            record.readName = readName;
            record.segment = this.indexToChr[refID];
            record._refID = refID;

            while (p < blockEnd) {
                var tag = String.fromCharCode(ba[p]) + String.fromCharCode(ba[p + 1]);
                var type = String.fromCharCode(ba[p + 2]);
                var value;

                if (type == 'A') {
                    value = String.fromCharCode(ba[p + 3]);
                    p += 4;
                } else if (type == 'i' || type == 'I') {
                    value = readInt(ba, p + 3);
                    p += 7;
                } else if (type == 'c' || type == 'C') {
                    value = ba[p + 3];
                    p += 4;
                } else if (type == 's' || type == 'S') {
                    value = readShort(ba, p + 3);
                    p += 5;
                } else if (type == 'f') {
                    throw 'FIXME need floats';
                } else if (type == 'Z') {
                    p += 3;
                    value = '';
                    for (;;) {
                        var cc = ba[p++];
                        if (cc == 0) {
                            break;
                        } else {
                            value += String.fromCharCode(cc);
                        }
                    }
                } else {
                    throw 'Unknown type '+ type;
                }
                record[tag] = value;
            }

            sink.push(record);

            offset = blockEnd;
        }
        // Exits via top of loop.
    }

});

return BamFile;

});<commit_msg>add caching of BAM records<commit_after>define( [
            'dojo/_base/declare',
            'dojo/_base/array',
            './Util',
            'JBrowse/Store/LRUCache'
        ],
        function( declare, array, BAMUtil, LRUCache ) {
//
// Binning (transliterated from SAM1.3 spec)
//

/* calculate bin given an alignment covering [beg,end) (zero-based, half-close-half-open) */
function reg2bin(beg, end)
{
    --end;
    if (beg>>14 == end>>14) return ((1<<15)-1)/7 + (beg>>14);
    if (beg>>17 == end>>17) return ((1<<12)-1)/7 + (beg>>17);
    if (beg>>20 == end>>20) return ((1<<9)-1)/7 + (beg>>20);
    if (beg>>23 == end>>23) return ((1<<6)-1)/7 + (beg>>23);
    if (beg>>26 == end>>26) return ((1<<3)-1)/7 + (beg>>26);
    return 0;
}

/* calculate the list of bins that may overlap with region [beg,end) (zero-based) */
var MAX_BIN = (((1<<18)-1)/7);
function reg2bins(beg, end)
{
    var i = 0, k, list = [];
    --end;
    list.push(0);
    for (k = 1 + (beg>>26); k <= 1 + (end>>26); ++k) list.push(k);
    for (k = 9 + (beg>>23); k <= 9 + (end>>23); ++k) list.push(k);
    for (k = 73 + (beg>>20); k <= 73 + (end>>20); ++k) list.push(k);
    for (k = 585 + (beg>>17); k <= 585 + (end>>17); ++k) list.push(k);
    for (k = 4681 + (beg>>14); k <= 4681 + (end>>14); ++k) list.push(k);
    return list;
}

var SEQRET_DECODER = ['=', 'A', 'C', 'x', 'G', 'x', 'x', 'x', 'T', 'x', 'x', 'x', 'x', 'x', 'x', 'N'];
var CIGAR_DECODER = ['M', 'I', 'D', 'N', 'S', 'H', 'P', '=', 'X', '?', '?', '?', '?', '?', '?', '?'];

var Vob = declare( null, {
    constructor: function(b, o) {
        this.block = b;
        this.offset = o;
    },
    toString: function() {
        return '' + this.block + ':' + this.offset;
    }
});

function readVob(ba, offset) {
    var block = ((ba[offset+6] & 0xff) * 0x100000000) + ((ba[offset+5] & 0xff) * 0x1000000) + ((ba[offset+4] & 0xff) * 0x10000) + ((ba[offset+3] & 0xff) * 0x100) + ((ba[offset+2] & 0xff));
    var bint = (ba[offset+1] << 8) | (ba[offset]);
    if (block == 0 && bint == 0) {
        return null;  // Should only happen in the linear index?
    } else {
        return new Vob(block, bint);
    }
}

var Chunk = declare( null, {
    constructor: function(minv, maxv) {
        this.minv = minv;
        this.maxv = maxv;
    }
});

var BamRecord = declare( null, {} );

var readInt = BAMUtil.readInt;
var readShort = BAMUtil.readInt;

var BamFile = declare( null,

/**
 * @lends JBrowse.Store.SeqFeature.BAM.File
 */
{

    /**
     * Low-level BAM file reading code.
     *
     * Adapted by Robert Buels from bam.js in the Dalliance Genome
     * Explorer which is copyright Thomas Down 2006-2010
     * @constructs
     */
    constructor: function() {},

    blocksForRange: function(refId, min, max) {
        var index = this.indices[refId];
        if (!index) {
            return [];
        }

        var intBinsL = reg2bins(min, max);
        var intBins = [];
        for (var i = 0; i < intBinsL.length; ++i) {
            intBins[intBinsL[i]] = true;
        }
        var leafChunks = [], otherChunks = [];

        var nbin = readInt(index, 0);
        var p = 4;
        for (var b = 0; b < nbin; ++b) {
            var bin = readInt(index, p);
            var nchnk = readInt(index, p+4);
    //        dlog('bin=' + bin + '; nchnk=' + nchnk);
            p += 8;
            if (intBins[bin]) {
                for (var c = 0; c < nchnk; ++c) {
                    var cs = readVob(index, p);
                    var ce = readVob(index, p + 8);
                    (bin < 4681 ? otherChunks : leafChunks).push(new Chunk(cs, ce));
                    p += 16;
                }
            } else {
                p +=  (nchnk * 16);
            }
        }
    //    dlog('leafChunks = ' + miniJSONify(leafChunks));
    //    dlog('otherChunks = ' + miniJSONify(otherChunks));

        var nintv = readInt(index, p);
        var lowest = null;
        var minLin = Math.min(min>>14, nintv - 1), maxLin = Math.min(max>>14, nintv - 1);
        for (var i = minLin; i <= maxLin; ++i) {
            var lb =  readVob(index, p + 4 + (i * 8));
            if (!lb) {
                continue;
            }
            if (!lowest || lb.block < lowest.block || lb.offset < lowest.offset) {
                lowest = lb;
            }
        }
        // dlog('Lowest LB = ' + lowest);

        var prunedOtherChunks = [];
        if (lowest != null) {
            for (var i = 0; i < otherChunks.length; ++i) {
                var chnk = otherChunks[i];
                if (chnk.maxv.block >= lowest.block && chnk.maxv.offset >= lowest.offset) {
                    prunedOtherChunks.push(chnk);
                }
            }
        }
        // dlog('prunedOtherChunks = ' + miniJSONify(prunedOtherChunks));
        otherChunks = prunedOtherChunks;

        var intChunks = [];
        for (var i = 0; i < otherChunks.length; ++i) {
            intChunks.push(otherChunks[i]);
        }
        for (var i = 0; i < leafChunks.length; ++i) {
            intChunks.push(leafChunks[i]);
        }

        intChunks.sort(function(c0, c1) {
            var dif = c0.minv.block - c1.minv.block;
            if (dif != 0) {
                return dif;
            } else {
                return c0.minv.offset - c1.minv.offset;
            }
        });
        var mergedChunks = [];
        if (intChunks.length > 0) {
            var cur = intChunks[0];
            for (var i = 1; i < intChunks.length; ++i) {
                var nc = intChunks[i];
                if (nc.minv.block == cur.maxv.block /* && nc.minv.offset == cur.maxv.offset */) { // no point splitting mid-block
                    cur = new Chunk(cur.minv, nc.maxv);
                } else {
                    mergedChunks.push(cur);
                    cur = nc;
                }
            }
            mergedChunks.push(cur);
        }
    //    dlog('mergedChunks = ' + miniJSONify(mergedChunks));

        return mergedChunks;
    },

    fetch: function(chr, min, max, callback) {
        var chrId = this.chrToIndex[chr];
        var chunks;
        if (chrId === undefined) {
            chunks = [];
        } else {
            chunks = this.blocksForRange(chrId, min, max);
            if (!chunks) {
                callback(null, 'Error in index fetch');
            }
        }

        chunks.toString = function() {
            var str = '';
            array.forEach( this, function(c) {
                str += c.minv+'..'+c.maxv+',';
            });
            return str;
        };

        this.recordCache = this.recordCache || new LRUCache({
            name: 'bamRecordCache',
            fillCallback: dojo.hitch(this, '_fetchChunkRecords' )
        });

        this.recordCache.get( chunks, function( records ) {
            records = array.filter( records, function( record ) {
                return (!min || record.pos <= max && record.pos + record.lseq >= min)
                    && (chrId === undefined || record._refID == chrId);
            });
            callback( records );
        });

    },

    _fetchChunkRecords: function( chunks, callback ) {
        var thisB = this;
        var records = [];
        var index = 0;
        var data;

        function tramp() {
            if (index >= chunks.length) {
                return callback(records);
            } else if (!data) {
                // dlog('fetching ' + index);
                var c = chunks[index];
                var fetchMin = c.minv.block;
                var fetchMax = c.maxv.block + (1<<16); // *sigh*
                thisB.data.slice(fetchMin, fetchMax - fetchMin)
                    .fetch(function(r) {
                               data = BAMUtil.unbgzf(r, c.maxv.block - c.minv.block + 1);
                               return tramp();
                           });
                return null;
            } else {
                var ba = new Uint8Array(data);
                thisB.readBamRecords(ba, chunks[index].minv.offset, records );
                data = null;
                ++index;
                return tramp();
            }
        };
        tramp();
    },

    readBamRecords: function(ba, offset, sink ) {
        while (true) {
            var blockSize = readInt(ba, offset);
            var blockEnd = offset + blockSize + 4;
            if (blockEnd >= ba.length) {
                return sink;
            }

            var record = new BamRecord();

            var refID = readInt(ba, offset + 4);
            var pos = readInt(ba, offset + 8);

            var bmn = readInt(ba, offset + 12);
            var bin = (bmn & 0xffff0000) >> 16;
            var mq = (bmn & 0xff00) >> 8;
            var nl = bmn & 0xff;

            var flag_nc = readInt(ba, offset + 16);
            var flag = (flag_nc & 0xffff0000) >> 16;
            var nc = flag_nc & 0xffff;

            var lseq = readInt(ba, offset + 20);
            record.lseq = lseq;

            var nextRef  = readInt(ba, offset + 24);
            var nextPos = readInt(ba, offset + 28);

            var tlen = readInt(ba, offset + 32);

            var readName = '';
            for (var j = 0; j < nl-1; ++j) {
                readName += String.fromCharCode(ba[offset + 36 + j]);
            }

            var p = offset + 36 + nl;

            var cigar = '';
            for (var c = 0; c < nc; ++c) {
                var cigop = readInt(ba, p);
                cigar = cigar + (cigop>>4) + CIGAR_DECODER[cigop & 0xf];
                p += 4;
            }
            record.cigar = cigar;

            var seq = '';
            var seqBytes = (lseq + 1) >> 1;
            for (var j = 0; j < seqBytes; ++j) {
                var sb = ba[p + j];
                seq += SEQRET_DECODER[(sb & 0xf0) >> 4];
                seq += SEQRET_DECODER[(sb & 0x0f)];
            }
            p += seqBytes;
            record.seq = seq;

            var qseq = '';
            for (var j = 0; j < lseq; ++j) {
                qseq += String.fromCharCode(ba[p + j]);
            }
            p += lseq;
            record.quals = qseq;

            record.pos = pos;
            record.MQ = mq;
            record.readName = readName;
            record.segment = this.indexToChr[refID];
            record._refID = refID;

            while (p < blockEnd) {
                var tag = String.fromCharCode(ba[p]) + String.fromCharCode(ba[p + 1]);
                var type = String.fromCharCode(ba[p + 2]);
                var value;

                if (type == 'A') {
                    value = String.fromCharCode(ba[p + 3]);
                    p += 4;
                } else if (type == 'i' || type == 'I') {
                    value = readInt(ba, p + 3);
                    p += 7;
                } else if (type == 'c' || type == 'C') {
                    value = ba[p + 3];
                    p += 4;
                } else if (type == 's' || type == 'S') {
                    value = readShort(ba, p + 3);
                    p += 5;
                } else if (type == 'f') {
                    throw 'FIXME need floats';
                } else if (type == 'Z') {
                    p += 3;
                    value = '';
                    for (;;) {
                        var cc = ba[p++];
                        if (cc == 0) {
                            break;
                        } else {
                            value += String.fromCharCode(cc);
                        }
                    }
                } else {
                    throw 'Unknown type '+ type;
                }
                record[tag] = value;
            }

            sink.push(record);

            offset = blockEnd;
        }
        // Exits via top of loop.
    }

});

return BamFile;

});<|endoftext|>


<commit_before>/**
 * ReportError component tests.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Internal dependencies
 */
import { createTestRegistry, provideModules } from '../../../tests/js/utils';
import {
	ERROR_CODE_MISSING_REQUIRED_SCOPE,
	ERROR_REASON_INSUFFICIENT_PERMISSIONS,
} from '../util/errors';
import { fireEvent, render } from '../../../tests/js/test-utils';
import ReportError from './ReportError';
import { MODULES_ANALYTICS } from '../modules/analytics/datastore/constants';

describe( 'ReportError', () => {
	let registry;
	let invalidateResolutionSpy;
	const moduleName = 'test-module';

	const errors = [
		{
			code: 'test-error-code',
			message: 'Test error message one',
			data: {
				reason: 'Data Error',
			},
			selectorData: {
				args: [
					{
						dimensions: [ 'ga:date' ],
						metrics: [ { expression: 'ga:users' } ],
						startDate: '2020-08-11',
						endDate: '2020-09-07',
					},
				],
				name: 'getReport',
				storeName: MODULES_ANALYTICS,
			},
		},
		{
			code: 'test-error-code',
			message: 'Test error message two',
			data: {
				reason: 'Data Error',
			},
			selectorData: {
				args: [
					{
						dimensions: [ 'ga:date' ],
						metrics: [ { expression: 'ga:users' } ],
						startDate: '2020-08-11',
						endDate: '2020-09-07',
					},
				],
				name: 'getReport',
				storeName: MODULES_ANALYTICS,
			},
		},
	];

	beforeEach( () => {
		registry = createTestRegistry();
		provideModules( registry, [
			{ slug: moduleName, name: 'Test Module' },
		] );
		invalidateResolutionSpy = jest.spyOn(
			registry.dispatch( MODULES_ANALYTICS ),
			'invalidateResolution'
		);
	} );

	afterEach( () => invalidateResolutionSpy.mockReset() );

	it( 'renders the error message', () => {
		const { container } = render(
			<ReportError
				moduleSlug={ moduleName }
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {},
				} }
			/>,
			{
				registry,
			}
		);

		expect( container.querySelector( 'p' ).textContent ).toEqual(
			'Test error message'
		);
	} );

	it( 'renders the error message without HTML tags', () => {
		const { container } = render(
			<ReportError
				moduleSlug={ moduleName }
				error={ {
					code: 'test-error-code',
					message:
						'
Test error message with HTML tags
',
					data: {},
				} }
			/>,
			{
				registry,
			}
		);

		expect( container.querySelector( 'p' ).textContent ).toEqual(
			'Test error message with HTML tags'
		);
	} );

	it( 'renders the insufficient permission error when ERROR_REASON_INSUFFICIENT_PERMISSIONS is provided as reason', () => {
		const { container } = render(
			<ReportError
				moduleSlug={ moduleName }
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: ERROR_REASON_INSUFFICIENT_PERMISSIONS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( container.querySelector( 'h3' ).textContent ).toEqual(
			'Insufficient permissions in Test Module'
		);
	} );

	it( "should not render the `Retry` button if the error's `selectorData.name` is not `getReport`", () => {
		const { queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: ERROR_REASON_INSUFFICIENT_PERMISSIONS,
					},
					selectorData: {
						args: [],
						name: 'getAccountID',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( queryByText( /retry/i ) ).not.toBeInTheDocument();
	} );

	it( 'should not render the `Retry` button if the error reason is `ERROR_REASON_INSUFFICIENT_PERMISSIONS`', () => {
		const { queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: ERROR_REASON_INSUFFICIENT_PERMISSIONS,
					},
					selectorData: {
						args: [],
						name: 'getAccountID',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( queryByText( /retry/i ) ).not.toBeInTheDocument();
	} );

	it( 'should not render the `Retry` button if the error reason is `ERROR_CODE_MISSING_REQUIRED_SCOPE`', () => {
		const { queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: ERROR_CODE_MISSING_REQUIRED_SCOPE,
					message: 'Test error message',
					data: {
						reason: '',
					},
					selectorData: {
						args: [],
						name: 'getAccountID',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( queryByText( /retry/i ) ).not.toBeInTheDocument();
	} );

	it( 'should not render the `Retry` button if the error is an auth error', () => {
		const { queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: '',
						reconnectURL: 'example.com',
					},
					selectorData: {
						args: [],
						name: 'getAccountID',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( queryByText( /retry/i ) ).not.toBeInTheDocument();
	} );

	it( 'should render the `Retry` button if the error selector name is `getReport`', () => {
		const { getByRole } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: '',
					},
					selectorData: {
						args: [
							{
								dimensions: [ 'ga:date' ],
								metrics: [ { expression: 'ga:users' } ],
								startDate: '2020-08-11',
								endDate: '2020-09-07',
							},
						],
						name: 'getReport',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( getByRole( 'button', { name: /retry/i } ) ).toBeInTheDocument();
	} );

	it( 'should dispatch the `invalidateResolution` action for each retry-able error', () => {
		const { getByRole } = render(
			<ReportError
				moduleSlug="analytics"
				error={ [
					...errors,
					// The following error object is not retry-able.
					...[
						{
							code: ERROR_CODE_MISSING_REQUIRED_SCOPE,
							message: 'Test error message',
							data: {
								reason: '',
							},
							selectorData: {
								args: [],
								name: 'getAccountID',
								storeName: MODULES_ANALYTICS,
							},
						},
					],
				] }
			/>,
			{
				registry,
			}
		);

		expect( getByRole( 'button', { name: /retry/i } ) ).toBeInTheDocument();

		fireEvent.click( getByRole( 'button', { name: /retry/i } ) );

		// There are three error object is being passed to the prop.
		// Only two are retry-able.
		// So, there should be only two invalidateResolution calls.
		expect( invalidateResolutionSpy ).toHaveBeenCalledTimes( 2 );
	} );

	it( 'should list all the error descriptions one by one if errors are different', () => {
		const { queryByText, getByRole } = render(
			<ReportError moduleSlug="analytics" error={ errors } />,
			{
				registry,
			}
		);

		expect( getByRole( 'button', { name: /retry/i } ) ).toBeInTheDocument();

		// Verify that the error descriptions are listed one by one if the errors are different.
		expect( queryByText( /Test error message one/i ) ).toBeInTheDocument();
		expect( queryByText( /Test error message two/i ) ).toBeInTheDocument();

		fireEvent.click( getByRole( 'button', { name: /retry/i } ) );

		expect( invalidateResolutionSpy ).toHaveBeenCalledTimes( 2 );
	} );

	it( 'should list only the unique error descriptions', () => {
		const { container, queryByText, getByRole } = render(
			<ReportError
				moduleSlug="analytics"
				error={ [ ...errors, ...errors ] }
			/>,
			{
				registry,
			}
		);

		expect( getByRole( 'button', { name: /retry/i } ) ).toBeInTheDocument();

		// Verify that the error descriptions are listed one by one if the errors are different.
		expect( queryByText( /Test error message one/i ) ).toBeInTheDocument();
		expect( queryByText( /Test error message two/i ) ).toBeInTheDocument();

		const errorDescriptionElement = container.querySelectorAll(
			'.googlesitekit-cta__description'
		);

		// Verify the child element count for the error description element is two.
		// However, the passed error array has four repetitive error objects.
		expect( errorDescriptionElement[ 0 ].childElementCount ).toBe( 2 );

		fireEvent.click( getByRole( 'button', { name: /retry/i } ) );
		expect( invalidateResolutionSpy ).toHaveBeenCalledTimes( 4 );
	} );

	it( 'shold render `Get help` ulink without prefix text on non-retryable error', () => {
		const { getByRole, queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				// Non-Retryable Error
				error={ {
					code: ERROR_CODE_MISSING_REQUIRED_SCOPE,
					message: 'Test error message',
					data: {
						reason: '',
					},
					selectorData: {
						args: [],
						name: 'getAccountID',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect(
			getByRole( 'link', { name: /get help/i } )
		).toBeInTheDocument();
		expect( queryByText( /retry didn’t work/i ) ).not.toBeInTheDocument();
	} );

	it( 'should render `Get help` link with prefix text on retryable error', () => {
		const { getByRole, queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				// Retryable Error
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: '',
					},
					selectorData: {
						args: [
							{
								dimensions: [ 'ga:date' ],
								metrics: [ { expression: 'ga:users' } ],
								startDate: '2020-08-11',
								endDate: '2020-09-07',
							},
						],
						name: 'getReport',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( getByRole( 'button', { name: /retry/i } ) ).toBeInTheDocument();
		expect(
			getByRole( 'link', { name: /get help/i } )
		).toBeInTheDocument();
		expect( queryByText( /retry didn’t work/i ) ).toBeInTheDocument();
	} );
} );
<commit_msg>Add a test case for request access button.<commit_after>/**
 * ReportError component tests.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Internal dependencies
 */
import {
	createTestRegistry,
	provideModuleRegistrations,
	provideModules,
	provideUserInfo,
	unsubscribeFromAll,
} from '../../../tests/js/utils';
import {
	ERROR_CODE_MISSING_REQUIRED_SCOPE,
	ERROR_REASON_INSUFFICIENT_PERMISSIONS,
} from '../util/errors';
import { fireEvent, render } from '../../../tests/js/test-utils';
import ReportError from './ReportError';
import { MODULES_ANALYTICS } from '../modules/analytics/datastore/constants';

describe( 'ReportError', () => {
	let registry;
	let invalidateResolutionSpy;
	const moduleName = 'test-module';

	const errors = [
		{
			code: 'test-error-code',
			message: 'Test error message one',
			data: {
				reason: 'Data Error',
			},
			selectorData: {
				args: [
					{
						dimensions: [ 'ga:date' ],
						metrics: [ { expression: 'ga:users' } ],
						startDate: '2020-08-11',
						endDate: '2020-09-07',
					},
				],
				name: 'getReport',
				storeName: MODULES_ANALYTICS,
			},
		},
		{
			code: 'test-error-code',
			message: 'Test error message two',
			data: {
				reason: 'Data Error',
			},
			selectorData: {
				args: [
					{
						dimensions: [ 'ga:date' ],
						metrics: [ { expression: 'ga:users' } ],
						startDate: '2020-08-11',
						endDate: '2020-09-07',
					},
				],
				name: 'getReport',
				storeName: MODULES_ANALYTICS,
			},
		},
	];

	beforeEach( () => {
		registry = createTestRegistry();
		provideModules( registry, [
			{ slug: moduleName, name: 'Test Module' },
		] );
		invalidateResolutionSpy = jest.spyOn(
			registry.dispatch( MODULES_ANALYTICS ),
			'invalidateResolution'
		);
	} );

	afterEach( () => {
		invalidateResolutionSpy.mockReset();
		unsubscribeFromAll( registry );
	} );

	it( 'renders the error message', () => {
		const { container } = render(
			<ReportError
				moduleSlug={ moduleName }
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {},
				} }
			/>,
			{
				registry,
			}
		);

		expect( container.querySelector( 'p' ).textContent ).toEqual(
			'Test error message'
		);
	} );

	it( 'renders the error message without HTML tags', () => {
		const { container } = render(
			<ReportError
				moduleSlug={ moduleName }
				error={ {
					code: 'test-error-code',
					message:
						'
Test error message with HTML tags
',
					data: {},
				} }
			/>,
			{
				registry,
			}
		);

		expect( container.querySelector( 'p' ).textContent ).toEqual(
			'Test error message with HTML tags'
		);
	} );

	it( 'renders the insufficient permission error when ERROR_REASON_INSUFFICIENT_PERMISSIONS is provided as reason', () => {
		const { container } = render(
			<ReportError
				moduleSlug={ moduleName }
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: ERROR_REASON_INSUFFICIENT_PERMISSIONS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( container.querySelector( 'h3' ).textContent ).toEqual(
			'Insufficient permissions in Test Module'
		);
	} );

	it( 'renders the insufficient permission error along with the `Request access` button if it exists for a module', () => {
		const userData = {
			id: 1,
			email: 'admin@example.com',
			name: 'admin',
			picture: 'https://path/to/image',
		};
		provideModules( registry, [
			{
				active: true,
				connected: true,
				slug: 'analytics',
			},
		] );
		provideModuleRegistrations( registry );
		provideUserInfo( registry, userData );

		const [ accountID, internalWebPropertyID, profileID ] = [
			'12345',
			'34567',
			'56789',
		];

		registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );
		registry
			.dispatch( MODULES_ANALYTICS )
			.setInternalWebPropertyID( internalWebPropertyID );
		registry.dispatch( MODULES_ANALYTICS ).setProfileID( profileID );

		const { container, queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: ERROR_REASON_INSUFFICIENT_PERMISSIONS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( container.querySelector( 'h3' ).textContent ).toEqual(
			'Insufficient permissions in Analytics'
		);
		// Verify the `Request access` button is rendered.
		expect( queryByText( /request access/i ) ).toBeInTheDocument();
	} );

	it( "should not render the `Retry` button if the error's `selectorData.name` is not `getReport`", () => {
		const { queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: ERROR_REASON_INSUFFICIENT_PERMISSIONS,
					},
					selectorData: {
						args: [],
						name: 'getAccountID',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( queryByText( /retry/i ) ).not.toBeInTheDocument();
	} );

	it( 'should not render the `Retry` button if the error reason is `ERROR_REASON_INSUFFICIENT_PERMISSIONS`', () => {
		const { queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: ERROR_REASON_INSUFFICIENT_PERMISSIONS,
					},
					selectorData: {
						args: [],
						name: 'getAccountID',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( queryByText( /retry/i ) ).not.toBeInTheDocument();
	} );

	it( 'should not render the `Retry` button if the error reason is `ERROR_CODE_MISSING_REQUIRED_SCOPE`', () => {
		const { queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: ERROR_CODE_MISSING_REQUIRED_SCOPE,
					message: 'Test error message',
					data: {
						reason: '',
					},
					selectorData: {
						args: [],
						name: 'getAccountID',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( queryByText( /retry/i ) ).not.toBeInTheDocument();
	} );

	it( 'should not render the `Retry` button if the error is an auth error', () => {
		const { queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: '',
						reconnectURL: 'example.com',
					},
					selectorData: {
						args: [],
						name: 'getAccountID',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( queryByText( /retry/i ) ).not.toBeInTheDocument();
	} );

	it( 'should render the `Retry` button if the error selector name is `getReport`', () => {
		const { getByRole } = render(
			<ReportError
				moduleSlug="analytics"
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: '',
					},
					selectorData: {
						args: [
							{
								dimensions: [ 'ga:date' ],
								metrics: [ { expression: 'ga:users' } ],
								startDate: '2020-08-11',
								endDate: '2020-09-07',
							},
						],
						name: 'getReport',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( getByRole( 'button', { name: /retry/i } ) ).toBeInTheDocument();
	} );

	it( 'should dispatch the `invalidateResolution` action for each retry-able error', () => {
		const { getByRole } = render(
			<ReportError
				moduleSlug="analytics"
				error={ [
					...errors,
					// The following error object is not retry-able.
					...[
						{
							code: ERROR_CODE_MISSING_REQUIRED_SCOPE,
							message: 'Test error message',
							data: {
								reason: '',
							},
							selectorData: {
								args: [],
								name: 'getAccountID',
								storeName: MODULES_ANALYTICS,
							},
						},
					],
				] }
			/>,
			{
				registry,
			}
		);

		expect( getByRole( 'button', { name: /retry/i } ) ).toBeInTheDocument();

		fireEvent.click( getByRole( 'button', { name: /retry/i } ) );

		// There are three error object is being passed to the prop.
		// Only two are retry-able.
		// So, there should be only two invalidateResolution calls.
		expect( invalidateResolutionSpy ).toHaveBeenCalledTimes( 2 );
	} );

	it( 'should list all the error descriptions one by one if errors are different', () => {
		const { queryByText, getByRole } = render(
			<ReportError moduleSlug="analytics" error={ errors } />,
			{
				registry,
			}
		);

		expect( getByRole( 'button', { name: /retry/i } ) ).toBeInTheDocument();

		// Verify that the error descriptions are listed one by one if the errors are different.
		expect( queryByText( /Test error message one/i ) ).toBeInTheDocument();
		expect( queryByText( /Test error message two/i ) ).toBeInTheDocument();

		fireEvent.click( getByRole( 'button', { name: /retry/i } ) );

		expect( invalidateResolutionSpy ).toHaveBeenCalledTimes( 2 );
	} );

	it( 'should list only the unique error descriptions', () => {
		const { container, queryByText, getByRole } = render(
			<ReportError
				moduleSlug="analytics"
				error={ [ ...errors, ...errors ] }
			/>,
			{
				registry,
			}
		);

		expect( getByRole( 'button', { name: /retry/i } ) ).toBeInTheDocument();

		// Verify that the error descriptions are listed one by one if the errors are different.
		expect( queryByText( /Test error message one/i ) ).toBeInTheDocument();
		expect( queryByText( /Test error message two/i ) ).toBeInTheDocument();

		const errorDescriptionElement = container.querySelectorAll(
			'.googlesitekit-cta__description'
		);

		// Verify the child element count for the error description element is two.
		// However, the passed error array has four repetitive error objects.
		expect( errorDescriptionElement[ 0 ].childElementCount ).toBe( 2 );

		fireEvent.click( getByRole( 'button', { name: /retry/i } ) );
		expect( invalidateResolutionSpy ).toHaveBeenCalledTimes( 4 );
	} );

	it( 'shold render `Get help` ulink without prefix text on non-retryable error', () => {
		const { getByRole, queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				// Non-Retryable Error
				error={ {
					code: ERROR_CODE_MISSING_REQUIRED_SCOPE,
					message: 'Test error message',
					data: {
						reason: '',
					},
					selectorData: {
						args: [],
						name: 'getAccountID',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect(
			getByRole( 'link', { name: /get help/i } )
		).toBeInTheDocument();
		expect( queryByText( /retry didn’t work/i ) ).not.toBeInTheDocument();
	} );

	it( 'should render `Get help` link with prefix text on retryable error', () => {
		const { getByRole, queryByText } = render(
			<ReportError
				moduleSlug="analytics"
				// Retryable Error
				error={ {
					code: 'test-error-code',
					message: 'Test error message',
					data: {
						reason: '',
					},
					selectorData: {
						args: [
							{
								dimensions: [ 'ga:date' ],
								metrics: [ { expression: 'ga:users' } ],
								startDate: '2020-08-11',
								endDate: '2020-09-07',
							},
						],
						name: 'getReport',
						storeName: MODULES_ANALYTICS,
					},
				} }
			/>,
			{
				registry,
			}
		);

		expect( getByRole( 'button', { name: /retry/i } ) ).toBeInTheDocument();
		expect(
			getByRole( 'link', { name: /get help/i } )
		).toBeInTheDocument();
		expect( queryByText( /retry didn't work/i ) ).toBeInTheDocument();
	} );
} );
<|endoftext|>


<commit_before>var search = (function () {

var exports = {};

var cached_term = "";
var cached_matches = [];
var cached_index;
var cached_table = $('table.focused_table');
var current_search_term;

// Data storage for the typeahead -- to go from object to string representation and vice versa.
var labels = [];
var mapped = {}; // to be deprecated
var search_object = {};

function get_query(obj) {
    return obj.query;
}

function get_person(obj) {
    return typeahead_helper.render_person(obj.query);
}

function phrase_match(phrase, q) {
    // match "tes" to "test" and "stream test" but not "hostess"
    var i;
    q = q.toLowerCase();

    var parts = phrase.split(' ');
    for (i = 0; i < parts.length; i++) {
        if (parts[i].toLowerCase().indexOf(q) === 0) {
            return true;
        }
    }
    return false;
}

function person_matches_query(person, q) {
    return phrase_match(person.full_name, q) || phrase_match(person.email, q);
}

function stream_matches_query(stream_name, q) {
    return phrase_match(stream_name, q);
}

// Convert a list of operators to a human-readable description.
function describe(operators) {
    return $.map(operators, function (elem) {
        var operand = elem[1];
        switch (elem[0]) {
        case 'is':
            if (operand === 'private') {
                return 'Narrow to all private messages';
            } else if (operand === 'starred') {
                return 'Narrow to starred messages';
            } else if (operand === 'mentioned') {
                return 'Narrow to mentioned messages';
            }
            break;

        case 'stream':
            return 'Narrow to stream ' + operand;

        case 'subject':
            return 'Narrow to subject ' + operand;

        case 'sender':
            return 'Narrow to sender ' + operand;

        case 'pm-with':
            return 'Narrow to private messages with ' + operand;

        case 'search':
            return 'Search for ' + operand;

        case 'in':
            return 'Narrow to messages in ' + operand;
        }
        return 'Narrow to (unknown operator)';
    }).join(', ');
}

function get_label(obj) {
    switch (obj.action) {
    case 'stream':
        return 'stream:' + obj.query;

    case 'private_message':
        return 'pm-with:' + obj.query.email;

    case 'sender':
        return 'sender:' + obj.query.email;

    case 'operators':
        return obj.query;
    }
}

exports.update_typeahead = function () {
    var stream_names = subs.subscribed_streams();
    stream_names.sort();

    var streams = $.map(stream_names, function (elt,idx) {
        return {action: 'stream', query: elt};
    });

    mapped = {};
    labels = [];
    $.each(streams, function (i, obj) {
        var label = get_label(obj);
        mapped[label] = obj;
        obj.label = label;
        labels.push(label);
    });
};

function narrow_or_search_for_term(item) {
    var search_query_box = $("#search_query");
    var obj = search_object[item];
    ui.change_tab_to('#home');
    switch (obj.action) {
    case 'stream':
        narrow.by('stream', obj.query, {trigger: 'search'});
        // It's sort of annoying that this is not in a position to
        // blur the search box, because it means that Esc won't
        // unnarrow, it'll leave the searchbox.

        // Narrowing will have already put some operators in the search box,
        // so leave the current text in.
        search_query_box.blur();
        return search_query_box.val();

    case 'private_message':
        narrow.by('pm-with', obj.query.email, {trigger: 'search'});
        search_query_box.blur();
        return search_query_box.val();

    case 'sender':
        narrow.by('sender', obj.query.email, {trigger: 'search'});
        search_query_box.blur();
        return search_query_box.val();

    case 'operators':
        narrow.activate(obj.operators, {trigger: 'search'});
        search_query_box.blur();
        return search_query_box.val();
    }
    return item;
}

function update_buttons_with_focus(focused) {
    var search_query = $('#search_query');

    // Show buttons iff the search input is focused, or has non-empty contents,
    // or we are narrowed.
    if (focused
        || search_query.val()
        || narrow.active()) {
        $('.search_button').removeAttr('disabled');
    } else {
        $('.search_button').attr('disabled', 'disabled');
    }
}

exports.update_button_visibility = function () {
    update_buttons_with_focus($('#search_query').is(':focus'));
};

function highlight_person(query, person) {
    var hilite = typeahead_helper.highlight_query_in_phrase;
    return hilite(query, person.full_name) + " <" + hilite(query, person.email) + ">";
}

function get_stream_suggestions(query) {
    var items = $.grep(labels, function (label) {
        var obj = mapped[label];
        if (obj.action === 'stream') {
            return stream_matches_query(obj.query, query);
        }
        return false;
    });
    var objs = $.map(items, function (label) {
        return mapped[label];
    });

    $.each(objs, function (idx, obj) {
        var prefix = 'Narrow to stream';
        var stream = obj.query;
        stream = typeahead_helper.highlight_query_in_phrase(query, stream);
        obj.description = prefix + ' ' + stream;
    });

    // streams are already sorted
    objs = typeahead_helper.sorter(query, objs, get_query);

     return objs;
}

function get_person_suggestions(all_people, query, action) {
    var people = $.grep(all_people, function (person) {
        return person_matches_query(person, query);
    });

    var objs = $.map(people, function (person) {
        return {action: action, query: person};
    });

    $.each(objs, function (idx, obj) {
        var prefix;
        var person;
        var name;

        if (action === 'private_message') {
            prefix = 'Narrow to private messages with';
        }

        if (action === 'sender') {
            prefix = 'Narrow to messages sent by';
        }

        person = obj.query;
        name = highlight_person(query, person);
        obj.description = prefix + ' ' + name;
        obj.label = get_label(obj);
    });

    objs.sort(function (x, y) {
        return typeahead_helper.compare_by_pms(get_query(x), get_query(y));
    });

    return objs;
}

exports.initialize = function () {
    $( "#search_query" ).typeahead({
        source: function (query, process) {
            var result = [];

            // Add an entry for narrow by operators.
            var operators = narrow.parse(query);
            if (operators.length !== 0) {
                var obj = {action: 'operators', query: query, operators: operators};
                var label = get_label(obj);
                obj.label = label;
                var description = describe(operators);
                obj.description = Handlebars.Utils.escapeExpression(description);
                result = [obj];
            } else {
                return [];
            }

            var stream_suggestions = get_stream_suggestions(query).slice(0,4);
            result = result.concat(stream_suggestions);

            var people = page_params.people_list;
            var person_suggestions;

            person_suggestions = get_person_suggestions(people, query, 'private_message').slice(0, 4);
            result = result.concat(person_suggestions);

            person_suggestions = get_person_suggestions(people, query, 'sender').slice(0, 4);
            result = result.concat(person_suggestions);

            // We can't send typeahead objects, only labels.
            search_object = {};
            $.each(result, function (idx, obj) {
                search_object[obj.label] = obj;
            });
            return $.map(result, function (obj) {
                return obj.label;
            });
        },
        items: 20,
        highlighter: function (item) {
            var obj = search_object[item];
            return obj.description;
        },
        matcher: function (item) {
            return true;
        },
        updater: narrow_or_search_for_term,
        sorter: function (items) {
            return items;
        }
    });

    $("#searchbox_form").keydown(function (e) {
        exports.update_button_visibility();
        var code = e.which;
        var search_query_box = $("#search_query");
        if (code === 13 && search_query_box.is(":focus")) {
            // Don't submit the form so that the typeahead can instead
            // handle our Enter keypress. Any searching that needs
            // to be done will be handled in the keyup.
            e.preventDefault();
            return false;
        }
    }).keyup(function (e) {
        var code = e.which;
        var search_query_box = $("#search_query");
        if (code === 13 && search_query_box.is(":focus")) {
            // We just pressed enter and the box had focus, which
            // means we didn't use the typeahead at all.  In that
            // case, we should act as though we're searching by
            // operators.  (The reason the other actions don't call
            // this codepath is that they first all blur the box to
            // indicate that they've done what they need to do)
            if (search_query_box.val().trim()) {
                narrow.activate(narrow.parse(search_query_box.val()));
            }
            search_query_box.blur();
            update_buttons_with_focus(false);
        }
    });

    // Some of these functions don't actually need to be exported,
    // but the code was moved here from elsewhere, and it would be
    // more work to re-order everything and make them private.
    $('#search_exit' ).on('click', exports.clear_search);

    var query = $('#search_query');
    query.on('focus', exports.focus_search)
         .on('blur' , function () {

        // The search query box is a visual cue as to
        // whether search or narrowing is active.  If
        // neither is active, we should clear the box on
        // blur.
        //
        // But we can't do this right away, because
        // selecting something in the typeahead menu causes
        // the box to lose focus a moment before.  We would
        // clear the thing we're about to search for.
        //
        // The workaround is to check 100ms later -- long
        // enough for the search to have gone through, but
        // short enough that the user won't notice (though
        // really it would be OK if they did).

        setTimeout(function () {
            if (!(narrow.active())) {
                query.val('');
            }
            exports.update_button_visibility();
        }, 100);
    });
};

function match_on_visible_text(row, search_term) {
    // You can't select on :visible, since that includes hidden elements that
    // take up space.
    return row.find(".message_content, .message_header")
              .text().toLowerCase().indexOf(search_term) !== -1;
}

exports.focus_search = function () {
    // The search bar is not focused yet, but will be.
    update_buttons_with_focus(true);
};

exports.initiate_search = function () {
    $('#search_query').select();
};

exports.clear_search = function () {
    narrow.deactivate();

    $('table tr').removeHighlight();
    $('#search_query').blur();
    exports.update_button_visibility();
};

return exports;

}());
<commit_msg>search.js: Simplify stream suggestions<commit_after>var search = (function () {

var exports = {};

var cached_term = "";
var cached_matches = [];
var cached_index;
var cached_table = $('table.focused_table');
var current_search_term;

// Data storage for the typeahead -- to go from object to string representation and vice versa.
var labels = [];
var mapped = {}; // to be deprecated
var search_object = {};

function get_query(obj) {
    return obj.query;
}

function get_person(obj) {
    return typeahead_helper.render_person(obj.query);
}

function phrase_match(phrase, q) {
    // match "tes" to "test" and "stream test" but not "hostess"
    var i;
    q = q.toLowerCase();

    var parts = phrase.split(' ');
    for (i = 0; i < parts.length; i++) {
        if (parts[i].toLowerCase().indexOf(q) === 0) {
            return true;
        }
    }
    return false;
}

function person_matches_query(person, q) {
    return phrase_match(person.full_name, q) || phrase_match(person.email, q);
}

function stream_matches_query(stream_name, q) {
    return phrase_match(stream_name, q);
}

// Convert a list of operators to a human-readable description.
function describe(operators) {
    return $.map(operators, function (elem) {
        var operand = elem[1];
        switch (elem[0]) {
        case 'is':
            if (operand === 'private') {
                return 'Narrow to all private messages';
            } else if (operand === 'starred') {
                return 'Narrow to starred messages';
            } else if (operand === 'mentioned') {
                return 'Narrow to mentioned messages';
            }
            break;

        case 'stream':
            return 'Narrow to stream ' + operand;

        case 'subject':
            return 'Narrow to subject ' + operand;

        case 'sender':
            return 'Narrow to sender ' + operand;

        case 'pm-with':
            return 'Narrow to private messages with ' + operand;

        case 'search':
            return 'Search for ' + operand;

        case 'in':
            return 'Narrow to messages in ' + operand;
        }
        return 'Narrow to (unknown operator)';
    }).join(', ');
}

function get_label(obj) {
    switch (obj.action) {
    case 'stream':
        return 'stream:' + obj.query;

    case 'private_message':
        return 'pm-with:' + obj.query.email;

    case 'sender':
        return 'sender:' + obj.query.email;

    case 'operators':
        return obj.query;
    }
}

exports.update_typeahead = function () {
    mapped = {};
    labels = []; // deprecated
};

function narrow_or_search_for_term(item) {
    var search_query_box = $("#search_query");
    var obj = search_object[item];
    ui.change_tab_to('#home');
    switch (obj.action) {
    case 'stream':
        narrow.by('stream', obj.query, {trigger: 'search'});
        // It's sort of annoying that this is not in a position to
        // blur the search box, because it means that Esc won't
        // unnarrow, it'll leave the searchbox.

        // Narrowing will have already put some operators in the search box,
        // so leave the current text in.
        search_query_box.blur();
        return search_query_box.val();

    case 'private_message':
        narrow.by('pm-with', obj.query.email, {trigger: 'search'});
        search_query_box.blur();
        return search_query_box.val();

    case 'sender':
        narrow.by('sender', obj.query.email, {trigger: 'search'});
        search_query_box.blur();
        return search_query_box.val();

    case 'operators':
        narrow.activate(obj.operators, {trigger: 'search'});
        search_query_box.blur();
        return search_query_box.val();
    }
    return item;
}

function update_buttons_with_focus(focused) {
    var search_query = $('#search_query');

    // Show buttons iff the search input is focused, or has non-empty contents,
    // or we are narrowed.
    if (focused
        || search_query.val()
        || narrow.active()) {
        $('.search_button').removeAttr('disabled');
    } else {
        $('.search_button').attr('disabled', 'disabled');
    }
}

exports.update_button_visibility = function () {
    update_buttons_with_focus($('#search_query').is(':focus'));
};

function highlight_person(query, person) {
    var hilite = typeahead_helper.highlight_query_in_phrase;
    return hilite(query, person.full_name) + " <" + hilite(query, person.email) + ">";
}

function get_stream_suggestions(query) {
    var streams = subs.subscribed_streams();

    streams = $.grep(streams, function (stream) {
        return stream_matches_query(stream, query);
    });

    var objs = $.map(streams, function (stream) {
        return {action: 'stream', query: stream};
    });

    $.each(objs, function (idx, obj) {
        var prefix = 'Narrow to stream';
        var stream = obj.query;
        stream = typeahead_helper.highlight_query_in_phrase(query, stream);
        obj.description = prefix + ' ' + stream;
        obj.label = get_label(obj);
    });

    objs = typeahead_helper.sorter(query, objs, get_query);

    return objs;
}

function get_person_suggestions(all_people, query, action) {
    var people = $.grep(all_people, function (person) {
        return person_matches_query(person, query);
    });

    var objs = $.map(people, function (person) {
        return {action: action, query: person};
    });

    $.each(objs, function (idx, obj) {
        var prefix;
        var person;
        var name;

        if (action === 'private_message') {
            prefix = 'Narrow to private messages with';
        }

        if (action === 'sender') {
            prefix = 'Narrow to messages sent by';
        }

        person = obj.query;
        name = highlight_person(query, person);
        obj.description = prefix + ' ' + name;
        obj.label = get_label(obj);
    });

    objs.sort(function (x, y) {
        return typeahead_helper.compare_by_pms(get_query(x), get_query(y));
    });

    return objs;
}

exports.initialize = function () {
    $( "#search_query" ).typeahead({
        source: function (query, process) {
            var result = [];

            // Add an entry for narrow by operators.
            var operators = narrow.parse(query);
            if (operators.length !== 0) {
                var obj = {action: 'operators', query: query, operators: operators};
                var label = get_label(obj);
                obj.label = label;
                var description = describe(operators);
                obj.description = Handlebars.Utils.escapeExpression(description);
                result = [obj];
            } else {
                return [];
            }

            var stream_suggestions = get_stream_suggestions(query).slice(0,4);
            result = result.concat(stream_suggestions);

            var people = page_params.people_list;
            var person_suggestions;

            person_suggestions = get_person_suggestions(people, query, 'private_message').slice(0, 4);
            result = result.concat(person_suggestions);

            person_suggestions = get_person_suggestions(people, query, 'sender').slice(0, 4);
            result = result.concat(person_suggestions);

            // We can't send typeahead objects, only labels.
            search_object = {};
            $.each(result, function (idx, obj) {
                search_object[obj.label] = obj;
            });
            return $.map(result, function (obj) {
                return obj.label;
            });
        },
        items: 20,
        highlighter: function (item) {
            var obj = search_object[item];
            return obj.description;
        },
        matcher: function (item) {
            return true;
        },
        updater: narrow_or_search_for_term,
        sorter: function (items) {
            return items;
        }
    });

    $("#searchbox_form").keydown(function (e) {
        exports.update_button_visibility();
        var code = e.which;
        var search_query_box = $("#search_query");
        if (code === 13 && search_query_box.is(":focus")) {
            // Don't submit the form so that the typeahead can instead
            // handle our Enter keypress. Any searching that needs
            // to be done will be handled in the keyup.
            e.preventDefault();
            return false;
        }
    }).keyup(function (e) {
        var code = e.which;
        var search_query_box = $("#search_query");
        if (code === 13 && search_query_box.is(":focus")) {
            // We just pressed enter and the box had focus, which
            // means we didn't use the typeahead at all.  In that
            // case, we should act as though we're searching by
            // operators.  (The reason the other actions don't call
            // this codepath is that they first all blur the box to
            // indicate that they've done what they need to do)
            if (search_query_box.val().trim()) {
                narrow.activate(narrow.parse(search_query_box.val()));
            }
            search_query_box.blur();
            update_buttons_with_focus(false);
        }
    });

    // Some of these functions don't actually need to be exported,
    // but the code was moved here from elsewhere, and it would be
    // more work to re-order everything and make them private.
    $('#search_exit' ).on('click', exports.clear_search);

    var query = $('#search_query');
    query.on('focus', exports.focus_search)
         .on('blur' , function () {

        // The search query box is a visual cue as to
        // whether search or narrowing is active.  If
        // neither is active, we should clear the box on
        // blur.
        //
        // But we can't do this right away, because
        // selecting something in the typeahead menu causes
        // the box to lose focus a moment before.  We would
        // clear the thing we're about to search for.
        //
        // The workaround is to check 100ms later -- long
        // enough for the search to have gone through, but
        // short enough that the user won't notice (though
        // really it would be OK if they did).

        setTimeout(function () {
            if (!(narrow.active())) {
                query.val('');
            }
            exports.update_button_visibility();
        }, 100);
    });
};

function match_on_visible_text(row, search_term) {
    // You can't select on :visible, since that includes hidden elements that
    // take up space.
    return row.find(".message_content, .message_header")
              .text().toLowerCase().indexOf(search_term) !== -1;
}

exports.focus_search = function () {
    // The search bar is not focused yet, but will be.
    update_buttons_with_focus(true);
};

exports.initiate_search = function () {
    $('#search_query').select();
};

exports.clear_search = function () {
    narrow.deactivate();

    $('table tr').removeHighlight();
    $('#search_query').blur();
    exports.update_button_visibility();
};

return exports;

}());
<|endoftext|>


<commit_before>var demos = [{"name": "Velocity addition",
              "steps": [{"caption": "Three stars. The left and right stars approach the center star at high speed - 0.9 times the speed of light.",
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Center", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -20, 0], "p": [2.1, 0], "m": 1, "label": "Left", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [30, -30, 0], "p": [-2.1, 0], "m": 1, "label": "Right", "shape": aSphere(5,64)}],
                         "options": {"c": 1},
                         "frame": 0},
                        {"caption": "Let's see that again, but this time from the perspective of the far-left star. The rightmost star only approaches at 0.995c — not the 1.8c you'd expect.",
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Center", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -20, 0], "p": [2.1, 0], "m": 1, "label": "Left", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [30, -30, 0], "p": [-2.1, 0], "m": 1, "label": "Right", "shape": aSphere(5,64)}],
                         "options": {"c": 1},
                         "frame": 1}
                       ]
             },
             {"name": "Relativistic Doppler shift",
              "steps": [{"caption": "First watch as Doppler shifting is turned off. A star shoots past.",
                         "options": {"showDoppler": false, "showGamma": false,"c": 1},
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Observer", "options": {"showVelocity": false}, "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -30, 0], "p": [1.5, 0], "m": 1, "label": "Star", "shape": aSphere(5,64)}
                                    ],
                         "frame": 0},
                        {"caption": "Now watch the same motion with the relativistic Doppler effect visible.",
                         "options": {"showDoppler": true, "c": 1},
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Observer", "shape": aSphere(5,64), "options": {"showVelocity": false}},
                                     {"object": extendedObject, "x": [-30, -30, 0], "p": [1.5, 0], "m": 1, "label": "Star", "shape": aSphere(5,64)}
                                    ],
                         "frame": 0}
                       ]
             },
             {"name": "Relativistic snake",
              "steps": [{"caption": "We have here two knives a fixed distance apart, moving downwards. A special breed of high-speed snake, Crotalus relativisticus, is moving at a speed close to the speed of light. It's just short enough to fit between the blades.",
                         "objects": [{"object": extendedObject, "x": [-27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[5, -10, 0, 0], [5, 10, 0, 0], [-5, -10, 0, 0], [5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[-5, -10, 0, 0], [-5, 10, 0, 0], [5, -10, 0, 0], [-5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [-68, 0, 0], "p": [8, 0, 0], "m": 1, "shape": linesPadder([[-35, 0, 0, 0], [35, 0, 0, 0]],3)}
                                    ],
                         "options": {"showFramePos": true, "showVisualPos": false, "showVelocity": true}},
                        {"caption": "But then consider the perspective of the snake. It's not length-contracted — the knives are. They're much closer together. How does the snake survive the knives?",
                         "objects": [{"object": extendedObject, "x": [-27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[5, -10, 0, 0], [5, 10, 0, 0], [-5, -10, 0, 0], [5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[-5, -10, 0, 0], [-5, 10, 0, 0], [5, -10, 0, 0], [-5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [-68, 0, 0], "p": [8, 0, 0], "m": 1, "shape": [[-35, 0, 0, 0], [35, 0, 0, 0]]}
                                    ],
                         "frame"  : 2,
                         "options": {"showFramePos": true, "showVisualPos": false}},
                       ]
             },
            {"name": "Test thingies.",
             "steps": [{"caption": "Test for a sphere",
                        "objects": [{"object": extendedObject, "x": [0, -32, 0], "p": [0,0,0], "m": 1, "shape": aSphere(10,100)},
                                    {"object": extendedObject, "x": [20, -20, -20], "p": [0,0,0], "m": 1,
                                            "options": {"temperature": 3000},
                                            "label": "A circle",   "shape": aCircle(10,30)}
                      ],
             "options": {"showFramePos": true, "showVisualPos": true, "show3D": true}}]},
            {"name": "Speed of light",
             "steps": [{"caption": "Two photons are emitted from the center of a ruler, heading outwards. Since they both travel at c, they reach the ends at the same time.",
                        "objects": [{"object": photon, "x": [0, 10, 0], "v": [1, 0, 0]},
                                    {"object": photon, "x": [0, 10, 0], "v": [-1, 0, 0]},
                                    {"object": extendedObject, "x": [0, 0, 0], "p": [0, 0, 0], "m": 1, "shape": linesPadder([[-100, 0, 0, 0], [100, 0, 0, 0]], 20)},
                                    {"object": extendedObject, "x": [0, 5, 0], "p": [5, 0, 0], "m": 1, "shape": aSphere(3, 50)}
                                    ],
                        "options": {"showFramePos": true, "showVisualPos": false}},
                       {"caption": "Now, the same scene, from the perspective of the observer travelling past at a high speed. Now the photons don't reach the ends of the ruler at the same time.",
                        "objects": [{"object": photon, "x": [0, 10, 0], "v": [1, 0, 0]},
                                    {"object": photon, "x": [0, 10, 0], "v": [-1, 0, 0]},
                                    {"object": extendedObject, "x": [0, 0, 0], "p": [0, 0, 0], "m": 1, "shape": linesPadder([[-100, 0, 0, 0], [100, 0, 0, 0]], 20)},
                                    {"object": extendedObject, "x": [0, 5, 0], "p": [5, 0, 0], "m": 1, "shape": aSphere(3, 50)}],
                        "frame": 3,
                        "options": {"showFramePos": true, "showVisualPos": false}}
                      ]
            }
            ];
<commit_msg>Clean up speed of light demo<commit_after>var demos = [{"name": "Velocity addition",
              "steps": [{"caption": "Three stars. The left and right stars approach the center star at high speed - 0.9 times the speed of light.",
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Center", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -20, 0], "p": [2.1, 0], "m": 1, "label": "Left", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [30, -30, 0], "p": [-2.1, 0], "m": 1, "label": "Right", "shape": aSphere(5,64)}],
                         "options": {"c": 1},
                         "frame": 0},
                        {"caption": "Let's see that again, but this time from the perspective of the far-left star. The rightmost star only approaches at 0.995c — not the 1.8c you'd expect.",
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Center", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -20, 0], "p": [2.1, 0], "m": 1, "label": "Left", "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [30, -30, 0], "p": [-2.1, 0], "m": 1, "label": "Right", "shape": aSphere(5,64)}],
                         "options": {"c": 1},
                         "frame": 1}
                       ]
             },
             {"name": "Relativistic Doppler shift",
              "steps": [{"caption": "First watch as Doppler shifting is turned off. A star shoots past.",
                         "options": {"showDoppler": false, "showGamma": false,"c": 1},
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Observer", "options": {"showVelocity": false}, "shape": aSphere(5,64)},
                                     {"object": extendedObject, "x": [-30, -30, 0], "p": [1.5, 0], "m": 1, "label": "Star", "shape": aSphere(5,64)}
                                    ],
                         "frame": 0},
                        {"caption": "Now watch the same motion with the relativistic Doppler effect visible.",
                         "options": {"showDoppler": true, "c": 1},
                         "objects": [{"object": extendedObject, "x": [0, -10, 0], "p": [0, 0], "m": 1, "label": "Observer", "shape": aSphere(5,64), "options": {"showVelocity": false}},
                                     {"object": extendedObject, "x": [-30, -30, 0], "p": [1.5, 0], "m": 1, "label": "Star", "shape": aSphere(5,64)}
                                    ],
                         "frame": 0}
                       ]
             },
             {"name": "Relativistic snake",
              "steps": [{"caption": "We have here two knives a fixed distance apart, moving downwards. A special breed of high-speed snake, Crotalus relativisticus, is moving at a speed close to the speed of light. It's just short enough to fit between the blades.",
                         "objects": [{"object": extendedObject, "x": [-27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[5, -10, 0, 0], [5, 10, 0, 0], [-5, -10, 0, 0], [5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[-5, -10, 0, 0], [-5, 10, 0, 0], [5, -10, 0, 0], [-5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [-68, 0, 0], "p": [8, 0, 0], "m": 1, "shape": linesPadder([[-35, 0, 0, 0], [35, 0, 0, 0]],3)}
                                    ],
                         "options": {"showFramePos": true, "showVisualPos": false, "showVelocity": true}},
                        {"caption": "But then consider the perspective of the snake. It's not length-contracted — the knives are. They're much closer together. How does the snake survive the knives?",
                         "objects": [{"object": extendedObject, "x": [-27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[5, -10, 0, 0], [5, 10, 0, 0], [-5, -10, 0, 0], [5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [27, -60, 0], "p": [0, 4, 0], "m": 1, "shape": [[-5, -10, 0, 0], [-5, 10, 0, 0], [5, -10, 0, 0], [-5, -10, 0, 0]]},
                                     {"object": extendedObject, "x": [-68, 0, 0], "p": [8, 0, 0], "m": 1, "shape": [[-35, 0, 0, 0], [35, 0, 0, 0]]}
                                    ],
                         "frame"  : 2,
                         "options": {"showFramePos": true, "showVisualPos": false}},
                       ]
             },
            {"name": "Test thingies.",
             "steps": [{"caption": "Test for a sphere",
                        "objects": [{"object": extendedObject, "x": [0, -32, 0], "p": [0,0,0], "m": 1, "shape": aSphere(10,100)},
                                    {"object": extendedObject, "x": [20, -20, -20], "p": [0,0,0], "m": 1,
                                            "options": {"temperature": 3000},
                                            "label": "A circle",   "shape": aCircle(10,30)}
                      ],
             "options": {"showFramePos": true, "showVisualPos": true, "show3D": true}}]},
            {"name": "Speed of light",
             "steps": [{"caption": "Two photons are emitted from the center of a ruler, heading outwards. Since they both travel at c, they reach the ends at the same time. But what happens from the perspective of the observer moving right?",
                        "objects": [{"object": photon, "x": [0, 10, 0], "v": [1, 0, 0]},
                                    {"object": photon, "x": [0, 10, 0], "v": [-1, 0, 0]},
                                    {"object": extendedObject, "x": [0, 0, 0], "p": [0, 0, 0], "m": 1, "shape": linesPadder([[-100, 0, 0, 0], [100, 0, 0, 0]], 20)},
                                    {"object": extendedObject, "x": [0, 5, 0], "p": [5, 0, 0], "m": 1, "shape": aSphere(3, 50)}
                                    ],
                        "options": {"showFramePos": true, "showVisualPos": false, "showGamma": false}},
                       {"caption": "This is the same scene from the perspective of the observer travelling right at a high speed. Now the photons don't reach the ends of the ruler at the same time. The speed of light is the same according to every observer.",
                        "objects": [{"object": photon, "x": [0, 10, 0], "v": [1, 0, 0]},
                                    {"object": photon, "x": [0, 10, 0], "v": [-1, 0, 0]},
                                    {"object": extendedObject, "x": [0, 0, 0], "p": [0, 0, 0], "m": 1, "shape": linesPadder([[-100, 0, 0, 0], [100, 0, 0, 0]], 20)},
                                    {"object": extendedObject, "x": [0, 5, 0], "p": [5, 0, 0], "m": 1, "shape": aSphere(3, 50)}],
                        "frame": 3,
                        "options": {"showFramePos": true, "showVisualPos": false, "showGamma": false}}
                      ]
            }
            ];
<|endoftext|>