mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
685 lines
22 KiB
JavaScript
685 lines
22 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
var Cc = SpecialPowers.Cc;
|
|
var Ci = SpecialPowers.Ci;
|
|
|
|
// Specifies whether we are using fake streams to run this automation
|
|
var FAKE_ENABLED = true;
|
|
try {
|
|
var audioDevice = SpecialPowers.getCharPref('media.audio_loopback_dev');
|
|
var videoDevice = SpecialPowers.getCharPref('media.video_loopback_dev');
|
|
dump('TEST DEVICES: Using media devices:\n');
|
|
dump('audio: ' + audioDevice + '\nvideo: ' + videoDevice + '\n');
|
|
FAKE_ENABLED = false;
|
|
} catch (e) {
|
|
dump('TEST DEVICES: No test devices found (in media.{audio,video}_loopback_dev, using fake streams.\n');
|
|
FAKE_ENABLED = true;
|
|
}
|
|
|
|
/**
|
|
* This class provides helpers around analysing the audio content in a stream
|
|
* using WebAudio AnalyserNodes.
|
|
*
|
|
* @constructor
|
|
* @param {object} stream
|
|
* A MediaStream object whose audio track we shall analyse.
|
|
*/
|
|
function AudioStreamAnalyser(ac, stream) {
|
|
this.audioContext = ac;
|
|
this.stream = stream;
|
|
this.sourceNode = this.audioContext.createMediaStreamSource(this.stream);
|
|
this.analyser = this.audioContext.createAnalyser();
|
|
// Setting values lower than default for speedier testing on emulators
|
|
this.analyser.smoothingTimeConstant = 0.2;
|
|
this.analyser.fftSize = 1024;
|
|
this.sourceNode.connect(this.analyser);
|
|
this.data = new Uint8Array(this.analyser.frequencyBinCount);
|
|
}
|
|
|
|
AudioStreamAnalyser.prototype = {
|
|
/**
|
|
* Get an array of frequency domain data for our stream's audio track.
|
|
*
|
|
* @returns {array} A Uint8Array containing the frequency domain data.
|
|
*/
|
|
getByteFrequencyData: function() {
|
|
this.analyser.getByteFrequencyData(this.data);
|
|
return this.data;
|
|
},
|
|
|
|
/**
|
|
* Append a canvas to the DOM where the frequency data are drawn.
|
|
* Useful to debug tests.
|
|
*/
|
|
enableDebugCanvas: function() {
|
|
var cvs = this.debugCanvas = document.createElement("canvas");
|
|
document.getElementById("content").appendChild(cvs);
|
|
|
|
// Easy: 1px per bin
|
|
cvs.width = this.analyser.frequencyBinCount;
|
|
cvs.height = 128;
|
|
cvs.style.border = "1px solid red";
|
|
|
|
var c = cvs.getContext('2d');
|
|
c.fillStyle = 'black';
|
|
|
|
var self = this;
|
|
function render() {
|
|
c.clearRect(0, 0, cvs.width, cvs.height);
|
|
var array = self.getByteFrequencyData();
|
|
for (var i = 0; i < array.length; i++) {
|
|
c.fillRect(i, (cvs.height - (array[i])), 1, cvs.height);
|
|
}
|
|
if (!cvs.stopDrawing) {
|
|
requestAnimationFrame(render);
|
|
}
|
|
}
|
|
requestAnimationFrame(render);
|
|
},
|
|
|
|
/**
|
|
* Stop drawing of and remove the debug canvas from the DOM if it was
|
|
* previously added.
|
|
*/
|
|
disableDebugCanvas: function() {
|
|
if (!this.debugCanvas || !this.debugCanvas.parentElement) {
|
|
return;
|
|
}
|
|
|
|
this.debugCanvas.stopDrawing = true;
|
|
this.debugCanvas.parentElement.removeChild(this.debugCanvas);
|
|
},
|
|
|
|
/**
|
|
* Return a Promise, that will be resolved when the function passed as
|
|
* argument, when called, returns true (meaning the analysis was a
|
|
* success).
|
|
*
|
|
* @param {function} analysisFunction
|
|
* A fonction that performs an analysis, and returns true if the
|
|
* analysis was a success (i.e. it found what it was looking for)
|
|
*/
|
|
waitForAnalysisSuccess: function(analysisFunction) {
|
|
var self = this;
|
|
return new Promise((resolve, reject) => {
|
|
function analysisLoop() {
|
|
var success = analysisFunction(self.getByteFrequencyData());
|
|
if (success) {
|
|
resolve();
|
|
return;
|
|
}
|
|
// else, we need more time
|
|
requestAnimationFrame(analysisLoop);
|
|
}
|
|
analysisLoop();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Return the FFT bin index for a given frequency.
|
|
*
|
|
* @param {double} frequency
|
|
* The frequency for whicht to return the bin number.
|
|
* @returns {integer} the index of the bin in the FFT array.
|
|
*/
|
|
binIndexForFrequency: function(frequency) {
|
|
return 1 + Math.round(frequency *
|
|
this.analyser.fftSize /
|
|
this.audioContext.sampleRate);
|
|
},
|
|
|
|
/**
|
|
* Reverse operation, get the frequency for a bin index.
|
|
*
|
|
* @param {integer} index an index in an FFT array
|
|
* @returns {double} the frequency for this bin
|
|
*/
|
|
frequencyForBinIndex: function(index) {
|
|
return (index - 1) *
|
|
this.audioContext.sampleRate /
|
|
this.analyser.fftSize;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates a MediaStream with an audio track containing a sine tone at the
|
|
* given frequency.
|
|
*
|
|
* @param {AudioContext} ac
|
|
* AudioContext in which to create the OscillatorNode backing the stream
|
|
* @param {double} frequency
|
|
* The frequency in Hz of the generated sine tone
|
|
* @returns {MediaStream} the MediaStream containing sine tone audio track
|
|
*/
|
|
function createOscillatorStream(ac, frequency) {
|
|
var osc = ac.createOscillator();
|
|
osc.frequency.value = frequency;
|
|
|
|
var oscDest = ac.createMediaStreamDestination();
|
|
osc.connect(oscDest);
|
|
osc.start();
|
|
return oscDest.stream;
|
|
}
|
|
|
|
/**
|
|
* Create the necessary HTML elements for head and body as used by Mochitests
|
|
*
|
|
* @param {object} meta
|
|
* Meta information of the test
|
|
* @param {string} meta.title
|
|
* Description of the test
|
|
* @param {string} [meta.bug]
|
|
* Bug the test was created for
|
|
* @param {boolean} [meta.visible=false]
|
|
* Visibility of the media elements
|
|
*/
|
|
function realCreateHTML(meta) {
|
|
var test = document.getElementById('test');
|
|
|
|
// Create the head content
|
|
var elem = document.createElement('meta');
|
|
elem.setAttribute('charset', 'utf-8');
|
|
document.head.appendChild(elem);
|
|
|
|
var title = document.createElement('title');
|
|
title.textContent = meta.title;
|
|
document.head.appendChild(title);
|
|
|
|
// Create the body content
|
|
var anchor = document.createElement('a');
|
|
anchor.textContent = meta.title;
|
|
if (meta.bug) {
|
|
anchor.setAttribute('href', 'https://bugzilla.mozilla.org/show_bug.cgi?id=' + meta.bug);
|
|
} else {
|
|
anchor.setAttribute('target', '_blank');
|
|
}
|
|
|
|
document.body.insertBefore(anchor, test);
|
|
|
|
var display = document.createElement('p');
|
|
display.setAttribute('id', 'display');
|
|
document.body.insertBefore(display, test);
|
|
|
|
var content = document.createElement('div');
|
|
content.setAttribute('id', 'content');
|
|
content.style.display = meta.visible ? 'block' : "none";
|
|
document.body.appendChild(content);
|
|
}
|
|
|
|
|
|
/**
|
|
* Create the HTML element if it doesn't exist yet and attach
|
|
* it to the content node.
|
|
*
|
|
* @param {string} type
|
|
* Type of media element to create ('audio' or 'video')
|
|
* @param {string} label
|
|
* Description to use for the element
|
|
* @return {HTMLMediaElement} The created HTML media element
|
|
*/
|
|
function createMediaElement(type, label) {
|
|
var id = label + '_' + type;
|
|
var element = document.getElementById(id);
|
|
|
|
// Sanity check that we haven't created the element already
|
|
if (element) {
|
|
return element;
|
|
}
|
|
|
|
element = document.createElement(type === 'audio' ? 'audio' : 'video');
|
|
element.setAttribute('id', id);
|
|
element.setAttribute('height', 100);
|
|
element.setAttribute('width', 150);
|
|
element.setAttribute('controls', 'controls');
|
|
element.setAttribute('autoplay', 'autoplay');
|
|
document.getElementById('content').appendChild(element);
|
|
|
|
return element;
|
|
}
|
|
|
|
|
|
/**
|
|
* Wrapper function for mediaDevices.getUserMedia used by some tests. Whether
|
|
* to use fake devices or not is now determined in pref further below instead.
|
|
*
|
|
* @param {Dictionary} constraints
|
|
* The constraints for this mozGetUserMedia callback
|
|
*/
|
|
function getUserMedia(constraints) {
|
|
info("Call getUserMedia for " + JSON.stringify(constraints));
|
|
return navigator.mediaDevices.getUserMedia(constraints)
|
|
.then(stream => (checkMediaStreamTracks(constraints, stream), stream));
|
|
}
|
|
|
|
// These are the promises we use to track that the prerequisites for the test
|
|
// are in place before running it.
|
|
var setTestOptions;
|
|
var testConfigured = new Promise(r => setTestOptions = r);
|
|
|
|
function setupEnvironment() {
|
|
if (!window.SimpleTest) {
|
|
// Running under Steeplechase
|
|
return;
|
|
}
|
|
|
|
// Running as a Mochitest.
|
|
SimpleTest.requestFlakyTimeout("WebRTC inherently depends on timeouts");
|
|
window.finish = () => SimpleTest.finish();
|
|
SpecialPowers.pushPrefEnv({
|
|
'set': [
|
|
['media.peerconnection.enabled', true],
|
|
['media.peerconnection.identity.enabled', true],
|
|
['media.peerconnection.identity.timeout', 120000],
|
|
['media.peerconnection.ice.stun_client_maximum_transmits', 14],
|
|
['media.peerconnection.ice.trickle_grace_period', 30000],
|
|
['media.navigator.permission.disabled', true],
|
|
['media.navigator.streams.fake', FAKE_ENABLED],
|
|
['media.getusermedia.screensharing.enabled', true],
|
|
['media.getusermedia.screensharing.allowed_domains', "mochi.test"],
|
|
['media.getusermedia.audiocapture.enabled', true],
|
|
['media.recorder.audio_node.enabled', true]
|
|
]
|
|
}, setTestOptions);
|
|
|
|
// We don't care about waiting for this to complete, we just want to ensure
|
|
// that we don't build up a huge backlog of GC work.
|
|
SpecialPowers.exactGC(window);
|
|
}
|
|
|
|
// This is called by steeplechase; which provides the test configuration options
|
|
// directly to the test through this function. If we're not on steeplechase,
|
|
// the test is configured directly and immediately.
|
|
function run_test(is_initiator,timeout) {
|
|
var options = { is_local: is_initiator,
|
|
is_remote: !is_initiator };
|
|
|
|
setTimeout(() => {
|
|
unexpectedEventArrived(new Error("PeerConnectionTest timed out after "+timeout+"s"));
|
|
}, timeout);
|
|
|
|
// Also load the steeplechase test code.
|
|
var s = document.createElement("script");
|
|
s.src = "/test.js";
|
|
s.onload = () => setTestOptions(options);
|
|
document.head.appendChild(s);
|
|
}
|
|
|
|
function runTestWhenReady(testFunc) {
|
|
setupEnvironment();
|
|
return testConfigured.then(options => testFunc(options))
|
|
.catch(e => ok(false, 'Error executing test: ' + e +
|
|
((typeof e.stack === 'string') ?
|
|
(' ' + e.stack.split('\n').join(' ... ')) : '')));
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks that the media stream tracks have the expected amount of tracks
|
|
* with the correct kind and id based on the type and constraints given.
|
|
*
|
|
* @param {Object} constraints specifies whether the stream should have
|
|
* audio, video, or both
|
|
* @param {String} type the type of media stream tracks being checked
|
|
* @param {sequence<MediaStreamTrack>} mediaStreamTracks the media stream
|
|
* tracks being checked
|
|
*/
|
|
function checkMediaStreamTracksByType(constraints, type, mediaStreamTracks) {
|
|
if (constraints[type]) {
|
|
is(mediaStreamTracks.length, 1, 'One ' + type + ' track shall be present');
|
|
|
|
if (mediaStreamTracks.length) {
|
|
is(mediaStreamTracks[0].kind, type, 'Track kind should be ' + type);
|
|
ok(mediaStreamTracks[0].id, 'Track id should be defined');
|
|
}
|
|
} else {
|
|
is(mediaStreamTracks.length, 0, 'No ' + type + ' tracks shall be present');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that the given media stream contains the expected media stream
|
|
* tracks given the associated audio & video constraints provided.
|
|
*
|
|
* @param {Object} constraints specifies whether the stream should have
|
|
* audio, video, or both
|
|
* @param {MediaStream} mediaStream the media stream being checked
|
|
*/
|
|
function checkMediaStreamTracks(constraints, mediaStream) {
|
|
checkMediaStreamTracksByType(constraints, 'audio',
|
|
mediaStream.getAudioTracks());
|
|
checkMediaStreamTracksByType(constraints, 'video',
|
|
mediaStream.getVideoTracks());
|
|
}
|
|
|
|
/**
|
|
* Check that a media stream contains exactly a set of media stream tracks.
|
|
*
|
|
* @param {MediaStream} mediaStream the media stream being checked
|
|
* @param {Array} tracks the tracks that should exist in mediaStream
|
|
* @param {String} [message] an optional message to pass to asserts
|
|
*/
|
|
function checkMediaStreamContains(mediaStream, tracks, message) {
|
|
message = message ? (message + ": ") : "";
|
|
tracks.forEach(t => ok(mediaStream.getTracks().includes(t),
|
|
message + "MediaStream " + mediaStream.id +
|
|
" contains track " + t.id));
|
|
is(mediaStream.getTracks().length, tracks.length,
|
|
message + "MediaStream " + mediaStream.id + " contains no extra tracks");
|
|
}
|
|
|
|
/*** Utility methods */
|
|
|
|
/** The dreadful setTimeout, use sparingly */
|
|
function wait(time) {
|
|
return new Promise(r => setTimeout(r, time));
|
|
}
|
|
|
|
/** The even more dreadful setInterval, use even more sparingly */
|
|
function waitUntil(func, time) {
|
|
return new Promise(resolve => {
|
|
var interval = setInterval(() => {
|
|
if (func()) {
|
|
clearInterval(interval);
|
|
resolve();
|
|
}
|
|
}, time || 200);
|
|
});
|
|
}
|
|
|
|
/** Time out while waiting for a promise to get resolved or rejected. */
|
|
var timeout = (promise, time, msg) =>
|
|
Promise.race([promise, wait(time).then(() => Promise.reject(new Error(msg)))]);
|
|
|
|
/** Use event listener to call passed-in function on fire until it returns true */
|
|
var listenUntil = (target, eventName, onFire) => {
|
|
return new Promise(resolve => target.addEventListener(eventName,
|
|
function callback() {
|
|
var result = onFire();
|
|
if (result) {
|
|
target.removeEventListener(eventName, callback, false);
|
|
resolve(result);
|
|
}
|
|
}, false));
|
|
};
|
|
|
|
/*** Test control flow methods */
|
|
|
|
/**
|
|
* Generates a callback function fired only under unexpected circumstances
|
|
* while running the tests. The generated function kills off the test as well
|
|
* gracefully.
|
|
*
|
|
* @param {String} [message]
|
|
* An optional message to show if no object gets passed into the
|
|
* generated callback method.
|
|
*/
|
|
function generateErrorCallback(message) {
|
|
var stack = new Error().stack.split("\n");
|
|
stack.shift(); // Don't include this instantiation frame
|
|
|
|
/**
|
|
* @param {object} aObj
|
|
* The object fired back from the callback
|
|
*/
|
|
return aObj => {
|
|
if (aObj) {
|
|
if (aObj.name && aObj.message) {
|
|
ok(false, "Unexpected callback for '" + aObj.name +
|
|
"' with message = '" + aObj.message + "' at " +
|
|
JSON.stringify(stack));
|
|
} else {
|
|
ok(false, "Unexpected callback with = '" + aObj +
|
|
"' at: " + JSON.stringify(stack));
|
|
}
|
|
} else {
|
|
ok(false, "Unexpected callback with message = '" + message +
|
|
"' at: " + JSON.stringify(stack));
|
|
}
|
|
throw new Error("Unexpected callback");
|
|
}
|
|
}
|
|
|
|
var unexpectedEventArrived;
|
|
var rejectOnUnexpectedEvent = new Promise((x, reject) => {
|
|
unexpectedEventArrived = reject;
|
|
});
|
|
|
|
/**
|
|
* Generates a callback function fired only for unexpected events happening.
|
|
*
|
|
* @param {String} description
|
|
Description of the object for which the event has been fired
|
|
* @param {String} eventName
|
|
Name of the unexpected event
|
|
*/
|
|
function unexpectedEvent(message, eventName) {
|
|
var stack = new Error().stack.split("\n");
|
|
stack.shift(); // Don't include this instantiation frame
|
|
|
|
return e => {
|
|
var details = "Unexpected event '" + eventName + "' fired with message = '" +
|
|
message + "' at: " + JSON.stringify(stack);
|
|
ok(false, details);
|
|
unexpectedEventArrived(new Error(details));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the one-shot event pattern used throughout. Each of the 'onxxx'
|
|
* attributes on the wrappers can be set with a custom handler. Prior to the
|
|
* handler being set, if the event fires, it causes the test execution to halt.
|
|
* That handler is used exactly once, after which the original, error-generating
|
|
* handler is re-installed. Thus, each event handler is used at most once.
|
|
*
|
|
* @param {object} wrapper
|
|
* The wrapper on which the psuedo-handler is installed
|
|
* @param {object} obj
|
|
* The real source of events
|
|
* @param {string} event
|
|
* The name of the event
|
|
*/
|
|
function createOneShotEventWrapper(wrapper, obj, event) {
|
|
var onx = 'on' + event;
|
|
var unexpected = unexpectedEvent(wrapper, event);
|
|
wrapper[onx] = unexpected;
|
|
obj[onx] = e => {
|
|
info(wrapper + ': "on' + event + '" event fired');
|
|
e.wrapper = wrapper;
|
|
wrapper[onx](e);
|
|
wrapper[onx] = unexpected;
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* This class executes a series of functions in a continuous sequence.
|
|
* Promise-bearing functions are executed after the previous promise completes.
|
|
*
|
|
* @constructor
|
|
* @param {object} framework
|
|
* A back reference to the framework which makes use of the class. It is
|
|
* passed to each command callback.
|
|
* @param {function[]} commandList
|
|
* Commands to set during initialization
|
|
*/
|
|
function CommandChain(framework, commandList) {
|
|
this._framework = framework;
|
|
this.commands = commandList || [ ];
|
|
}
|
|
|
|
CommandChain.prototype = {
|
|
/**
|
|
* Start the command chain. This returns a promise that always resolves
|
|
* cleanly (this catches errors and fails the test case).
|
|
*/
|
|
execute: function () {
|
|
return this.commands.reduce((prev, next, i) => {
|
|
if (typeof next !== 'function' || !next.name) {
|
|
throw new Error('registered non-function' + next);
|
|
}
|
|
|
|
return prev.then(() => {
|
|
info('Run step ' + (i + 1) + ': ' + next.name);
|
|
return Promise.race([ next(this._framework), rejectOnUnexpectedEvent ]);
|
|
});
|
|
}, Promise.resolve())
|
|
.catch(e =>
|
|
ok(false, 'Error in test execution: ' + e +
|
|
((typeof e.stack === 'string') ?
|
|
(' ' + e.stack.split('\n').join(' ... ')) : '')));
|
|
},
|
|
|
|
/**
|
|
* Add new commands to the end of the chain
|
|
*/
|
|
append: function(commands) {
|
|
this.commands = this.commands.concat(commands);
|
|
},
|
|
|
|
/**
|
|
* Returns the index of the specified command in the chain.
|
|
* @param {start} Optional param specifying the index at which the search will
|
|
* start. If not specified, the search starts at index 0.
|
|
*/
|
|
indexOf: function(functionOrName, start) {
|
|
start = start || 0;
|
|
if (typeof functionOrName === 'string') {
|
|
var index = this.commands.slice(start).findIndex(f => f.name === functionOrName);
|
|
if (index !== -1) {
|
|
index += start;
|
|
}
|
|
return index;
|
|
}
|
|
return this.commands.indexOf(functionOrName, start);
|
|
},
|
|
|
|
mustHaveIndexOf: function(functionOrName, start) {
|
|
var index = this.indexOf(functionOrName, start);
|
|
if (index == -1) {
|
|
throw new Error("Unknown test: " + functionOrName);
|
|
}
|
|
return index;
|
|
},
|
|
|
|
/**
|
|
* Inserts the new commands after the specified command.
|
|
*/
|
|
insertAfter: function(functionOrName, commands, all, start) {
|
|
this._insertHelper(functionOrName, commands, 1, all, start);
|
|
},
|
|
|
|
/**
|
|
* Inserts the new commands after every occurrence of the specified command
|
|
*/
|
|
insertAfterEach: function(functionOrName, commands) {
|
|
this._insertHelper(functionOrName, commands, 1, true);
|
|
},
|
|
|
|
/**
|
|
* Inserts the new commands before the specified command.
|
|
*/
|
|
insertBefore: function(functionOrName, commands, all, start) {
|
|
this._insertHelper(functionOrName, commands, 0, all, start);
|
|
},
|
|
|
|
_insertHelper: function(functionOrName, commands, delta, all, start) {
|
|
var index = this.mustHaveIndexOf(functionOrName);
|
|
start = start || 0;
|
|
for (; index !== -1; index = this.indexOf(functionOrName, index)) {
|
|
if (!start) {
|
|
this.commands = [].concat(
|
|
this.commands.slice(0, index + delta),
|
|
commands,
|
|
this.commands.slice(index + delta));
|
|
if (!all) {
|
|
break;
|
|
}
|
|
} else {
|
|
start -= 1;
|
|
}
|
|
index += (commands.length + 1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes the specified command, returns what was removed.
|
|
*/
|
|
remove: function(functionOrName) {
|
|
return this.commands.splice(this.mustHaveIndexOf(functionOrName), 1);
|
|
},
|
|
|
|
/**
|
|
* Removes all commands after the specified one, returns what was removed.
|
|
*/
|
|
removeAfter: function(functionOrName, start) {
|
|
return this.commands.splice(this.mustHaveIndexOf(functionOrName, start) + 1);
|
|
},
|
|
|
|
/**
|
|
* Removes all commands before the specified one, returns what was removed.
|
|
*/
|
|
removeBefore: function(functionOrName) {
|
|
return this.commands.splice(0, this.mustHaveIndexOf(functionOrName));
|
|
},
|
|
|
|
/**
|
|
* Replaces a single command, returns what was removed.
|
|
*/
|
|
replace: function(functionOrName, commands) {
|
|
this.insertBefore(functionOrName, commands);
|
|
return this.remove(functionOrName);
|
|
},
|
|
|
|
/**
|
|
* Replaces all commands after the specified one, returns what was removed.
|
|
*/
|
|
replaceAfter: function(functionOrName, commands, start) {
|
|
var oldCommands = this.removeAfter(functionOrName, start);
|
|
this.append(commands);
|
|
return oldCommands;
|
|
},
|
|
|
|
/**
|
|
* Replaces all commands before the specified one, returns what was removed.
|
|
*/
|
|
replaceBefore: function(functionOrName, commands) {
|
|
var oldCommands = this.removeBefore(functionOrName);
|
|
this.insertBefore(functionOrName, commands);
|
|
return oldCommands;
|
|
},
|
|
|
|
/**
|
|
* Remove all commands whose name match the specified regex.
|
|
*/
|
|
filterOut: function (id_match) {
|
|
this.commands = this.commands.filter(c => !id_match.test(c.name));
|
|
},
|
|
};
|
|
|
|
|
|
function IsMacOSX10_6orOlder() {
|
|
if (navigator.platform.indexOf("Mac") !== 0) {
|
|
return false;
|
|
}
|
|
|
|
var version = Cc["@mozilla.org/system-info;1"]
|
|
.getService(Ci.nsIPropertyBag2)
|
|
.getProperty("version");
|
|
// the next line is correct: Mac OS 10.6 corresponds to Darwin version 10.x !
|
|
// Mac OS 10.7 is Darwin version 11.x. the |version| string we've got here
|
|
// is the Darwin version.
|
|
return (parseFloat(version) < 11.0);
|
|
}
|
|
|
|
(function(){
|
|
var el = document.createElement("link");
|
|
el.rel = "stylesheet";
|
|
el.type = "text/css";
|
|
el.href= "/tests/SimpleTest/test.css";
|
|
document.head.appendChild(el);
|
|
}());
|