Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,128 @@
(function(window) {
/**
* Keep awake utility to ensure device does
* not go to sleep before the test completes.
*/
var KeepAwake = (function() {
var running = false,
power = navigator.mozPower;
return {
_activate: function ka_activate() {
if (running) {
try {
power.screenEnabled = true;
} catch (e) {
//during desktop we don't care if this
//fails. In app context it will always work.
}
setTimeout(ka_activate, 800);
}
},
start: function ka_start() {
running = true;
this._activate();
},
stop: function ka_stop() {
running = false;
}
};
}());
/**
* For CI (and maybe for other uses)
* we want to configure the agent
* with url options.
*
* Options are taken from the hash rather
* then the query string for future use
* of offline cache.
*
* http://test-agent.gaiamobile.org/index.html#?key=value
*/
var AgentConfig = (function() {
var params = window.location.hash,
index = params.indexOf('?'),
options = {
websocketUrl: null
};
if (index > -1) {
params.slice(index + 1).split('&').forEach(function(pair) {
var split = pair.split('=');
if (split.length === 2) {
options[split[0]] = split[1];
}
});
}
return options;
}());
console.log(AgentConfig);
var worker = new TestAgent.BrowserWorker({
/* this is where your tests will be loaded into */
sandbox: './sandbox.html'
});
worker.use(TestAgent.BrowserWorker.Config, {
/* config file which lists all available tests for the ui */
url: './config.json'
});
worker.use(TestAgent.BrowserWorker.Websocket, {
url: AgentConfig.websocketUrl
});
worker.use(TestAgent.BrowserWorker.MultiDomainDriver, {
groupTestsByDomain: function(test) {
var parsed = TestUrlResolver.parse(test);
var env = parsed.host + '-' + parsed.url;
var result = {
domain: parsed.domain + 'test/unit/_proxy.html',
test: '/' + parsed.url,
env: env
};
return result;
}
});
worker.use(TestAgent.BrowserWorker.TestUi);
worker.use(TestAgent.BrowserWorker.ErrorReporting);
worker.use(TestAgent.Common.MochaTestEvents, {
defaultMochaReporter: 'HTML'
});
worker.use(TestAgent.Common.BlanketReportCollector);
worker.on({
'open': function() {
setTimeout(worker.ready.bind(worker), 0);
},
'test runner': function() {
KeepAwake.start();
},
'test runner end': function() {
KeepAwake.stop();
}
});
worker.config();
worker.start();
}(this));

View file

@ -0,0 +1,71 @@
(function(window) {
var worker = new TestAgent.BrowserWorker({
/* this is where your tests will be loaded into */
sandbox: '/test/unit/_sandbox.html'
});
worker.use(TestAgent.BrowserWorker.PostMessage);
worker.use(TestAgent.BrowserWorker.MochaDriver, {
ui: 'tdd',
/* path to mocha */
mochaUrl: CommonResourceLoader.url('/common/vendor/mocha/mocha.js'),
/**
* Important: will not cause failures if missing.
* Allows each app to have its own setup.js file that will execute
* before any tests are loaded so we can utilize helpers outside
* of setup blocks. Any files require from setup.js should also
* block loading of any tests...
*/
testHelperUrl: [
CommonResourceLoader.url('/common/test/helper.js'),
CommonResourceLoader.url('/common/test/sinon_helper.js'),
'/test/unit/setup.js'
],
reporter: null,
setup: {
ignoreLeaks: true
}
});
worker.use(TestAgent.BrowserWorker.BlanketDriver, {
blanketUrl: CommonResourceLoader.url('/common/vendor/blanket/blanket.js'),
defaultConfig: {
'data-cover-only': 'js/',
'data-cover-never': '["test/unit/","alameda.js","/shared/"]',
'data-cover-flags': 'lazyload',
'data-cover-adapter': CommonResourceLoader
.url('/common/vendor/blanket/test-agent-blanket.js')
}
});
//enable let, yield, etc...
worker.loader.type = 'application/javascript;version=1.8';
worker.on({
'sandbox': function() {
},
'run tests': function() {
},
'open': function() {
console.log('socket open');
},
'close': function() {
console.log('lost client trying to reconnect');
}
});
worker.start();
}(this));

View file

@ -0,0 +1,49 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Serve the tests</title>
<script type="text/javascript" charset="utf-8">
(function(window){
var Loader = window.CommonResourceLoader = {},
host = document.location.host,
domain = host;
Loader.domain = document.location.protocol + '//' + domain;
Loader.url = function(url){
return this.domain + url;
}
Loader.script = function(url, doc){
doc = doc || document;
doc.write('<script type="application/javascript;version=1.8" src="' + this.url(url) + '"><\/script>');
return this;
};
}(this));
</script>
<style type="text/css" media="all">
html,body,iframe {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<!-- Test Agent UI will be loaded in here -->
<div id="test-agent-ui">
</div>
<script type="text/javascript" charset="utf-8">
CommonResourceLoader.
script('/common/test/test_url_resolver.js').
script('/common/vendor/test-agent/test-agent.js').
script('/common/test/agent_proxy.js');
</script>
</body>
</html>

View file

@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Tests</title>
<style type="text/css" media="all">
iframe {
border: none;
padding: 0px;
}
</style>
<script type="text/javascript" charset="utf-8">
</script>
</head>
<body>
<div id="mocha">
</div>
<div id="test">
</div>
</body>
</html>

View file

@ -0,0 +1,133 @@
// put stuff here to help your tests out...
(function(window) {
// register the global
window.navigator;
var htmlFragments;
var requestedFragments = {};
var Common = window.parent.CommonResourceLoader,
// mocha test methods we want to provide
// yield support to.
testMethods = [
'suiteSetup',
'setup',
'test',
'teardown',
'suiteTeardown'
];
// chai has no backtraces in ff
// this patch will change the error
// class used to provide real .stack.
function patchChai(chai, util) {
var Assertion = chai.Assertion;
function chaiAssert(expr, msg, negateMsg, expected, actual) {
var ok = util.test(this, arguments);
if (!ok) {
var msg = util.getMessage(this, arguments);
throw new Error(
// include custom message if available
this.msg ? this.msg + ': ' + msg : msg
);
}
}
Assertion.prototype.assert = chaiAssert;
}
window.requireApp = function(url, cb, options) {
require(TestUrlResolver.resolve(url), cb, options);
};
/**
* Appends a templated node to the body for a suite
* Removes the node at teardown.
* @param {String} is the type of element.
* @param {Object} attrs optional attributes.
*/
window.suiteTemplate = function(is, attrs) {
var testElement;
setup(function ta_template() {
var foundElement = htmlFragments.querySelector('element[name="' + is + '"]');
testElement = document.createElement(foundElement.getAttribute('extends') || 'div');
var template = foundElement.querySelector('template');
testElement.innerHTML = template.innerHTML;
attrs = attrs || {};
for (var i in attrs) {
testElement.setAttribute(i, attrs[i]);
}
document.body.appendChild(testElement);
});
teardown(function ta_teardown() {
testElement.parentNode.removeChild(testElement);
});
};
window.requireElements = function(url) {
url = TestUrlResolver.resolve(url);
if (requestedFragments[url]) {
return;
}
requestedFragments[url] = true;
var xhr = new XMLHttpRequest();
xhr.open('GET', url, false /* intentional sync */);
xhr.send();
if (!htmlFragments) {
htmlFragments = document.createElement('div');
}
htmlFragments.innerHTML += xhr.responseText;
};
/**
* Require a file from /common/ resources.
*
* Usage: requireCommon('vendor/mocha/mocha.js');
*
* @param {String} url relative location of file.
* @param {Function} cb optional callback called
* when resource has been loaded.
*/
window.requireCommon = function(url, cb) {
require(Common.url('/common/' + url), cb);
};
// template
requireCommon('test/template.js');
// load chai
window.requireCommon('vendor/chai/chai.js', function() {
chai.config.includeStack = true;
chai.use(patchChai);
window.requireCommon('vendor/chai/chai-as-promised.js');
window.assert = chai.assert;
window.expect = chai.expect;
window.should = chai.should();
});
// mocha helpers
window.requireCommon('test/mocha_task.js');
window.requireCommon('test/mocha_generators.js', function() {
testMethods.forEach(function(method) {
testSupport.mochaGenerators.overload(method);
});
});
// url utilities
window.requireCommon('test/test_url_resolver.js');
}(this));

View file

@ -0,0 +1,89 @@
(function(window) {
var support;
if (typeof(window.testSupport) === 'undefined') {
window.testSupport = {};
}
support = window.testSupport;
support.mochaGenerators = {
/**
* Overloads a test function to be capable
* of using generators/yield for control flow.
*
* test('yield', function(){
* function sleep(time) {
* setTimeout(time, MochaTask.next);
* }
*
* yield sleep(5);
*
* //..
*
* });
*
*
* @param {String} type name of mocha function.
*/
overload: function overload(type) {
var orig = window[type];
/**
* async tests fall off the stack
* bring them back by providing the done fn
*/
function wrapDone(done) {
return function(val) {
if (typeof(val) === 'function') {
try {
val();
return done();
} catch (e) {
return done(e);
}
} else {
return done.apply(this, arguments);
}
}
}
window[type] = function() {
var args = Array.prototype.slice.call(arguments),
cb = args.pop();
function wrapper(origDone) {
var gen, useDone = cb.length > 0;
var done = wrapDone(origDone);
if (useDone) {
gen = cb.call(this, done);
} else {
gen = cb.call(this);
}
if (gen && gen.next) {
MochaTask.start(gen, function(e) {
done();
}, function(e) {
done(e);
});
} else {
if (!useDone) {
done();
}
}
}
wrapper.toString = function() {
return cb.toString();
}
args.push(wrapper);
orig.apply(window, args);
}
}
};
}(this));

View file

@ -0,0 +1,126 @@
/**
* MochaTask manager.
* Used to provide generator yields for mocha test.
*/
var MochaTask = (function() {
var current, done, errorHandler;
function clearState() {
done = null;
current = null;
errorHandler = null;
}
return {
/**
* Starts a task this will
* effect the global state of MochaTask.
*
*
* @param {Function} generator generator function.
* @param {Function} success success callback.
* @param {Function} error error callback receives an Error instance.
*/
start: function(generator, success, error) {
current = generator;
if (!current.next) {
current = generator.call(this);
}
done = success;
errorHandler = error;
if (current && current.next) {
current.next();
}
return this;
},
/**
* Sends next value to the generator.
* Function may be passed to functions
* that normally require a callback.
*
* yield setTimeout(100, MochaTask.next)
*
* If next is called with a value it will be passed
* to the generators send method so you can use it to
* create this kind of code.
*
* var responseText = yield magicXhrMethod('GET', url, Mocha.next);
*
* @param {Object} value object to pass to generator.
*/
next: function(value) {
//assign references so we
//can clear state without messing
//up execution order later.
var complete = done,
generator = current,
handler = errorHandler;
try {
generator.send(value);
} catch (e) {
if (e instanceof StopIteration) {
clearState();
if (complete) {
complete();
}
} else {
clearState();
if (handler) {
handler(e);
}
}
}
},
nextNodeStyle: function(error, value) {
//assign references so we
//can clear state without messing
//up execution order later.
var complete = done,
generator = current,
handler = errorHandler;
if (error) {
try {
generator.throw(error);
} catch (e) {
if (!(e instanceof StopIteration)) {
handler(e);
} else {
clearState();
if (complete) {
complete();
}
}
}
return;
}
try {
generator.send(value);
} catch (e) {
if (e instanceof StopIteration) {
clearState();
if (complete) {
complete();
}
} else {
clearState();
if (handler) {
handler(e);
}
}
}
}
};
}());

View file

@ -0,0 +1,13 @@
// load sinon.js
window.requireCommon('vendor/sinon/sinon.js', function() {
setup(function() {
this.sinon = sinon.sandbox.create();
});
teardown(function() {
if (this.sinon) {
this.sinon.restore();
this.sinon = null;
}
});
});

View file

@ -0,0 +1,49 @@
* { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }
body,
html {
height: 100%;
}
body {
font: 14px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
}
body > iframe {
border: 1px solid #CCC;
width: 100%;
height: 100%;
}
#test-agent-ui {
margin-bottom: 5px;
}
#test-agent-ui ul {
padding: none;
border: none;
clear: both;
margin-top: 10px;
}
#test-agent-ui li {
list-style: none;
padding: 0px;
margin: 0px;
float: left;
width: 33%;
}
#test-agent-ui li.active {
font-weight: bold;
}
#test-agent-ui button {
clear: both;
font-size: 14px;
width: 100%;
padding: 5px;
height: 30px;
border: 1px solid #E2E2E2;
background-color: #FAFAFA;
}

View file

@ -0,0 +1,591 @@
//
// SyntheticGestures.js: test utilities for generating gesture events.
//
// This module defines a SyntheticGestures object whose properties are
// utility functions that generate sequences of synthetic events. The event
// sequences are intended to be recognized as gestures by GestureDetector.js
// so this module is useful for writing tests of UI code that uses
// GestureDetector.js.
//
// SyntheticGestures.tap(): emit touch events to generate a tap gesture
// on the specified target element. GestureDetector.js should emit
// a tap event in response. SyntheticGesture.mousetap() is the same
// but synthesizes mouse events instead.
//
// SyntheticGestures.dbltap(): emit touch events to generate a double
// tap gesture on the specified target element. GestureDetector.js
// should emit two tap events and a dbltap event in response.
// SyntheticGesture.mousedbltap() is the same but synthesizes mouse
// events instead of touch events.
//
// SyntheticGestures.swipe(): emit touch events for a basic one finger
// touch, move, lift geture over a specifed target, with given start
// and end coordinates and gesture duration. GestureDetector.js should
// emit a series of pan events and a final swipe event.
// SyntheticGestures.mouseswipe() is similar but uses mouse events
// instead of touch events.
//
// SyntheticGestures.hold(): emit touch events for a touch/hold/move/release
// gesture. GestureDetector should emit a holdstart/holdmove/holdend
// sequence of events in response. SyntheticGesture.mousehold() is
// similar but uses mouse events instead.
//
// SyntheticGestures.pinch(): emit touch events for a 2-finger pinch
// open or pinch close gesture with given staring points for each
// finger and a given scale factor. GestureDetector.js should
// respond with a sequence of transform events culminating in one
// with something very close to the specified scale factor.
// Two-finger gestures can't be simulated with a mouse, so there is no
// mouse-only version of this gesture, and the pinch() function will
// fail on platforms that do not support touch events.
//
// The coordinates passed to these functions are relative to the
// coordinate system of the target element. If you pass numbers, they
// are interpreted as pixels relative to upper-left corner of the
// target. If you pass a string, then it is parsed to a number. If a
// string-valued coordinate ends with '%', then it is interpreted as a
// percent of the target element's width or height, otherwise it is
// interpreted as a number of pixels. For the swipe(), mouseswipe(),
// hold(), and mousehold() functions, if the second two coordinates
// are strings that begin with "+" or "-" then they are interpreted
// relative to the first two coordinates. These relative values can be
// specifed as percentages or as pixels.
//
var SyntheticGestures = (function() {
// Send move events about this often
var EVENT_INTERVAL = 30; // milliseconds
// The current array of all pending touches
var touches = [];
// For assigning unique ids to all touches
var nextTouchId = 1000;
// Emit a touch event of the specified type, using touch as the sole
// member of the changedTouches property
function emitTouchEvent(type, touch) {
var target = touch.target;
var doc = target.ownerDocument;
var win = doc.defaultView;
// Create the three lists of touches
// All touches in the same document
var documentTouches = doc.createTouchList(touches.filter(function(t) {
return t.target.ownerDocument === doc;
}));
// All touches on the same target
var targetTouches = doc.createTouchList(touches.filter(function(t) {
return t.target === target;
}));
// Just this one touch that changed
var changedTouches = doc.createTouchList(touch);
// Create the event object
var event = document.createEvent('TouchEvent');
event.initTouchEvent(type,
true, // bubles
true, // cancellable
win,
0, // detail
false, false, false, false, // no modifier keys
documentTouches,
targetTouches,
changedTouches);
// Now dispatch it
target.dispatchEvent(event);
}
//
// Dispatch a touchstart, touchmove+, touchend sequence of events to
// simulate a single-finger touch of the specified duration.
//
// The xt and yt arguments are functions that must return x and y
// coordinates of the touch at any time t, 0 <= t <= duration.
// Alternatively, xt and yt can be two-element arrays [from,to], and
// the mouse coordinates will be interpolated between those
// coordinates. The then argument is an optional callback that will
// be invoked after the touchend event is sent. In either case, the
// coordinates should be window coordinates, relative to the viewport
//
// touch() can be called multiple times concurrently to simulate
// 2 and 3 finger gestures.
//
// This is a low-level function that the higher-level touch gesture
// utilities are built on. Most testing code will not need to call it.
//
function touch(target, duration, xt, yt, then) {
var doc = target.ownerDocument;
var win = doc.defaultView;
var touchId = nextTouchId++;
var x = xt;
if (typeof xt !== 'function') {
x = function(t) { return xt[0] + t / duration * (xt[1] - xt[0]); };
}
var y = yt;
if (typeof yt !== 'function') {
y = function(t) { return yt[0] + t / duration * (yt[1] - yt[0]); };
}
// viewport coordinates
var clientX = Math.round(x(0)), clientY = Math.round(y(0));
// document coordinates
var pageX = clientX + win.pageXOffset,
pageY = clientY + win.pageYOffset;
// screen coordinates
var screenX = clientX + win.mozInnerScreenX,
screenY = clientY + win.mozInnerScreenY;
// Remember the coordinates
var lastX = clientX, lastY = clientY;
// Create the touch object we'll be
var touch = doc.createTouch(win, target, touchId,
pageX, pageY,
screenX, screenY,
clientX, clientY);
// Add this new touch to the list of touches
touches.push(touch);
// Send the start event
emitTouchEvent('touchstart', touch);
var startTime = Date.now();
setTimeout(nextEvent, EVENT_INTERVAL);
function nextEvent() {
// Figure out if this is the last of the touchmove events
var time = Date.now();
var dt = time - startTime;
var last = dt + EVENT_INTERVAL / 2 > duration;
// Find our touch object in the touches[] array.
// Note that its index may have changed since we pushed it
var touchIndex = touches.indexOf(touch);
// If this is the last move event, make sure we move all the way
if (last)
dt = duration;
// New coordinates of the touch
clientX = Math.round(x(dt));
clientY = Math.round(y(dt));
// If we've moved, send a move event
if (clientX !== lastX || clientY !== lastY) { // If we moved
lastX = clientX;
lastY = clientY;
pageX = clientX + win.pageXOffset;
pageY = clientY + win.pageYOffset;
screenX = clientX + win.mozInnerScreenX;
screenY = clientY + win.mozInnerScreenY;
// Since we moved, we've got to create a new Touch object
// with the new coordinates
touch = doc.createTouch(win, target, touchId,
pageX, pageY,
screenX, screenY,
clientX, clientY);
// Replace the old touch object with the new one
touches[touchIndex] = touch;
// And send the touchmove event
emitTouchEvent('touchmove', touch);
}
// If that was the last move, send the touchend event
// and call the callback
if (last) {
touches.splice(touchIndex, 1);
emitTouchEvent('touchend', touch);
if (then)
setTimeout(then, 0);
}
// Otherwise, schedule the next event
else {
setTimeout(nextEvent, EVENT_INTERVAL);
}
}
}
function coordinates(target, x0, y0, x1, y1) {
var coords = {};
var box = target.getBoundingClientRect();
var tx0 = typeof x0;
var ty0 = typeof y0;
var tx1 = typeof x1;
var ty1 = typeof y1;
function percent(s, x) {
s = s.trim();
var f = parseFloat(s);
if (s[s.length - 1] === '%')
f = f * x / 100;
return f;
}
function relative(s, x) {
var factor;
if (s[0] === '+')
factor = 1;
else
factor = -1;
return factor * percent(s.substring(1), x);
}
if (tx0 === 'number')
coords.x0 = box.left + x0;
else if (tx0 === 'string')
coords.x0 = box.left + percent(x0, box.width);
if (tx1 === 'number')
coords.x1 = box.left + x1;
else if (tx1 === 'string') {
x1 = x1.trim();
if (x1[0] === '+' || x1[0] === '-')
coords.x1 = coords.x0 + relative(x1, box.width);
else
coords.x1 = box.left + percent(x1, box.width);
}
if (ty0 === 'number')
coords.y0 = box.top + y0;
else if (ty0 === 'string')
coords.y0 = box.top + percent(y0, box.height);
if (ty1 === 'number')
coords.y1 = box.top + y1;
else if (ty1 === 'string') {
y1 = y1.trim();
if (y1[0] === '+' || y1[0] === '-')
coords.y1 = coords.y0 + relative(y1, box.height);
else
coords.y1 = box.top + percent(y1, box.height);
}
return coords;
}
// Dispatch touchstart and touchend events over the specified target
// and then invoke the then() callback.
// x and y are the relative to the viewport.
// If not specified then the center of the target is used. t is the
// optional amount of time between touchstart and touchend event.
function tap(target, then, x, y, t) {
if (!SyntheticGestures.touchSupported) {
console.warn('tap: touch events not supported; using mouse instead');
return mousetap(target, then, x, y, t);
}
if (x == null)
x = '50%';
if (y == null)
y = '50%';
var c = coordinates(target, x, y);
touch(target, t || 50, [c.x0, c.x0], [c.y0, c.y0], then);
}
// Dispatch a dbltap gesture. The arguments are like those to tap()
// except that interval is the time between taps rather than the time between
// touchstart and touchend
function dbltap(target, then, x, y, interval) {
if (!SyntheticGestures.touchSupported) {
console.warn('dbltap: touch events not supported; using mouse instead');
return mousedbltap(target, then, x, y, interval);
}
if (x == null)
x = '50%';
if (y == null)
y = '50%';
var c = coordinates(target, x, y);
touch(target, 25, [c.x0, c.x0], [c.y0, c.y0], function() {
// When the first tap is done, start a timer for interval ms.
setTimeout(function() {
// After interval ms, send the second tap
touch(target, 25, [c.x0, c.x0], [c.y0, c.y0], then);
}, interval || 50);
});
}
// Swipe smoothly from (x1, y1) to (x2, y2) over duration ms
// then invoke the then() callback.
function swipe(target, x1, y1, x2, y2, duration, then) {
if (!SyntheticGestures.touchSupported) {
console.warn('swipe: touch events not supported; using mouse instead');
return mouseswipe(target, x1, y1, x2, y2, duration, then);
}
var c = coordinates(target, x1, y1, x2, y2);
touch(target, duration || 200, [c.x0, c.x1], [c.y0, c.y1], then);
}
// Begin a touch at x1,y1 and hold it for holdtime ms,
// then move smoothly to x2,y2 over movetime ms, and then invoke then().
function hold(target, holdtime, x1, y1, x2, y2, movetime, then) {
if (!SyntheticGestures.touchSupported) {
console.warn('hold: touch events not supported; using mouse instead');
return mousehold(target, holdtime, x1, y1, x2, y2, movetime, then);
}
if (!movetime)
movetime = 200;
var c = coordinates(target, x1, y1, x2, y2);
touch(target, holdtime + movetime,
function(t) { // x coordinate a function of t
if (t < holdtime)
return c.x0;
else
return c.x0 + (t - holdtime) / movetime * (c.x1 - c.x0);
},
function(t) { // y coordinate a function of t
if (t < holdtime)
return c.y0;
else
return c.y0 + (t - holdtime) / movetime * (c.y1 - c.y0);
},
then);
}
// Begin touches at (x1,y1) and (x2,y2) and then move them smoothly toward
// or away from each other by the specified scale factor over duration ms.
// Finally, invoke the then() callback
function pinch(target, x1, y1, x2, y2, scale, duration, then) {
if (!SyntheticGestures.touchSupported) {
console.error('pinch: touch events not supported on this platform');
return;
}
var c1 = coordinates(target, x1, y1);
var c2 = coordinates(target, x2, y2);
x1 = c1.x0;
y1 = c1.y0;
x2 = c2.x0;
y2 = c2.y0;
var xmid = (x1 + x2) / 2;
var ymid = (y1 + y2) / 2;
var newx1 = Math.round(xmid + (x1 - xmid) * scale);
var newy1 = Math.round(ymid + (y1 - ymid) * scale);
var newx2 = Math.round(xmid + (x2 - xmid) * scale);
var newy2 = Math.round(ymid + (y2 - ymid) * scale);
// Emit two concurrent series of touch events.a
// The first one is simple:
touch(target, duration, [x1, newx1], [y1, newy1]);
// The second touch moves twice as fast and then holds still and
// lasts for an extra 200ms to ensure that both moves complete
// before either finger lifts up. Hopefully this means we'll get
// the full scale effect
touch(target, duration + 200,
function(t) {
if (t < duration / 2)
return x2 + t * 2 / duration * (newx2 - x2);
else
return newx2;
},
function(t) {
if (t < duration / 2)
return y2 + t * 2 / duration * (newy2 - y2);
else
return newy2;
},
then);
}
//
// Dispatch a mousedown, mousemove+, mouseup sequence of events over
// duration ms to simulate a click-drag motion of the mouse or trackpad.
//
// The xt and yt arguments are [x0,x1], [y0,y1] or are functions
// of t that specify the mouse coordinates at time t just as for the
// touch() function.
//
// Unlike the touch() function, this drag() function takes a document
// argument instead of an element argument, and always determines the
// target of its events using document.elementFromPoint(). Callers
// must ensure that xt and yt specify an initial point inside the
// desired target element.
//
// This is a low-level function that the higher-level mouse gesture
// utilities are built on. Most testing code will not need to call it.
//
function drag(doc, duration, xt, yt, then, detail, button) {
var win = doc.defaultView;
detail = detail || 1;
button = button || 0;
var x = xt;
if (typeof xt !== 'function') {
x = function(t) { return xt[0] + t / duration * (xt[1] - xt[0]); };
}
var y = yt;
if (typeof yt !== 'function') {
y = function(t) { return yt[0] + t / duration * (yt[1] - yt[0]); };
}
// viewport coordinates
var clientX = Math.round(x(0)), clientY = Math.round(y(0));
// Remember the coordinates
var lastX = clientX, lastY = clientY;
// Send the initial mousedown event
mouseEvent('mousedown', clientX, clientY);
// Now send a sequence of mousemove events followed by mouse up
var startTime = Date.now();
setTimeout(nextEvent, EVENT_INTERVAL);
// Send a mouse event of the specified type to whatever element is
// at the specified coordinates in the viewport.
function mouseEvent(type, clientX, clientY) {
// Figure out what element the mouse would be over at (x,y)
var target = doc.elementFromPoint(clientX, clientY);
// Create an event
var mousedown = doc.createEvent('MouseEvent');
// Initialize it
mousedown.initMouseEvent(type,
true, true, // bubbles, cancellable
win, detail, // window, click count
clientX + win.mozInnerScreenX,
clientY + win.mozInnerScreenY,
clientX, clientY,
false, false, false, false, // keyboard modifiers
button, null); // mouse button, related target
// And dispatch it on the target element
target.dispatchEvent(mousedown);
}
function nextEvent() {
// Figure out if we've sent all of the mousemove events
var time = Date.now();
var dt = time - startTime;
// Is this the last move event we'll be sending?
var last = dt + EVENT_INTERVAL / 2 > duration;
// If so make sure we move all the way
if (last)
dt = duration;
// New coordinates of the touch
clientX = Math.round(x(dt));
clientY = Math.round(y(dt));
// If we moved, send a move event
if (clientX !== lastX || clientY !== lastY) { // If we moved
lastX = clientX;
lastY = clientY;
mouseEvent('mousemove', clientX, clientY);
}
// If this was the last move, send a mouse up and call the callback
// Otherwise, schedule the next move event
if (last) {
mouseEvent('mouseup', lastX, lastY);
if (then)
setTimeout(then, 0);
}
else {
setTimeout(nextEvent, EVENT_INTERVAL);
}
}
}
// Send a mousedown/mouseup pair
// XXX: will the browser automatically follow this with a click event?
function mousetap(target, then, x, y, t) {
if (x == null)
x = '50%';
if (y == null)
y = '50%';
var c = coordinates(target, x, y);
drag(target.ownerDocument, t || 50, [c.x0, c.x0], [c.y0, c.y0], then);
}
// Dispatch a dbltap gesture. The arguments are like those to tap()
// except that interval is the time between taps rather than the time between
// touchstart and touchend
function mousedbltap(target, then, x, y, interval) {
if (x == null)
x = '50%';
if (y == null)
y = '50%';
var c = coordinates(target, x, y);
drag(target.ownerDocument, 25, [c.x0, c.x0], [c.y0, c.y0], function() {
// When the first tap is done, start a timer for interval ms.
setTimeout(function() {
// After interval ms, send the second tap with the click count set to 2.
drag(target.ownerDocument, 25, [c.x0, c.x0], [c.y0, c.y0], then, 2);
}, interval || 50);
});
}
// Swipe smoothly with the mouse from (x1, y1) to (x2, y2) over duration ms
// then invoke the then() callback.
function mouseswipe(target, x1, y1, x2, y2, duration, then) {
var c = coordinates(target, x1, y1, x2, y2);
drag(target.ownerDocument, duration || 200,
[c.x0, c.x1], [c.y0, c.y1], then);
}
// Mousedown at x1,y1 and hold it for holdtime ms,
// then move smoothly to x2,y2 over movetime ms, then mouse up,
// and then invoke then().
function mousehold(target, holdtime, x1, y1, x2, y2, movetime, then) {
if (!movetime)
movetime = 200;
var c = coordinates(target, x1, y1, x2, y2);
drag(target.ownerDocument, holdtime + movetime,
function(t) { // x coordinate a function of t
if (t < holdtime)
return c.x0;
else
return c.x0 + (t - holdtime) / movetime * (c.x1 - c.x0);
},
function(t) { // y coordinate a function of t
if (t < holdtime)
return c.y0;
else
return c.y0 + (t - holdtime) / movetime * (c.y1 - c.y0);
},
then);
}
return {
// in the context of node, we don't even have document....
touchSupported: (typeof document !== 'undefined' && typeof document.createTouch === 'function'),
tap: tap,
mousetap: mousetap,
dbltap: dbltap,
mousedbltap: mousedbltap,
swipe: swipe,
mouseswipe: mouseswipe,
hold: hold,
mousehold: mousehold,
pinch: pinch // There is no mouse-based alternative to this
};
}());

View file

@ -0,0 +1,47 @@
(function(window) {
if (typeof(testSupport) === 'undefined') {
testSupport = {};
}
function template(url) {
url = TestUrlResolver.resolve(url);
teardown(function() {
var test = document.getElementById('test');
if (test) {
test.parentNode.removeChild(test);
}
});
setup(function(done) {
var test = document.getElementById('test');
if (!test) {
test = document.createElement('div');
test.id = 'test';
document.body.appendChild(test);
}
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function(event) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var wrapper = document.createElement('div');
wrapper.innerHTML = xhr.responseText;
test.appendChild(wrapper.firstChild);
done();
} else {
done(new Error(
'could not load template file "' + url + '"'
));
}
}
};
xhr.send(null);
});
}
testSupport.template = template;
}(this));

View file

@ -0,0 +1,47 @@
(function(window) {
var TestUrlResolver = (function() {
let location = window.location,
domainParts = window.location.host.split('.'),
addSubdomain = false,
prefix = null;
if (domainParts.length > 2) {
//running from gaiamobile.org subdomain
addSubdomain = true;
} else {
//running from localhost
addSubdomain = false;
}
return {
PARSE_REGEX: /^(\/?)([\w\d-]+)\/(.*)/,
parse: function tur_parse(url) {
if (addSubdomain) {
let parsedUrl = this.PARSE_REGEX.exec(url);
let domain = location.protocol + '//' + parsedUrl[2] + '.';
domain += domainParts.slice(1).join('.') + '/';
return {
domain: domain,
host: parsedUrl[2],
url: parsedUrl[3]
};
} else {
throw new Error('you must run tests using real domains on localhost');
}
},
resolve: function tur_resolve(url) {
var parts = this.parse(url);
return parts.domain + parts.url;
}
};
}());
window.TestUrlResolver = TestUrlResolver;
}(this));