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

14
gaia/tests/.jshintrc Executable file
View file

@ -0,0 +1,14 @@
{
"extends": "../.jshintrc",
"predef": [
"client",
"config",
"test",
"setup",
"marionette",
"marionetteScriptFinished",
"module",
"require",
"process"
]
}

124
gaia/tests/atoms/accessibility.js Executable file
View file

@ -0,0 +1,124 @@
/* 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';
/* globals Components, marionetteScriptFinished */
/* exported Accessibility */
var Accessibility = {
_accRetrieval: Components.classes[
'@mozilla.org/accessibleRetrieval;1'].getService(
Components.interfaces.nsIAccessibleRetrieval),
_getAccessible:
function Accessibility__getAccessible(element, callback, once) {
let attempts = 500;
let intervalId = setInterval(() => {
let acc = this._accRetrieval.getAccessibleFor(element);
if (acc || --attempts <= 0 || once) {
clearInterval(intervalId);
if (attempts <= 0) {
let tagName = element ? element.tagName : undefined;
let elemId = element ? element.id : undefined;
console.log('accessibility.js: failed to get accessible for tag=\"' +
tagName + '\" id=\"' + elemId + '\"');
}
try {
callback(acc);
} catch (e) {
marionetteScriptFinished({ error: e.message });
}
}
}, 10);
},
_matchState: function Accessibility__matchState(acc, stateName) {
let stateToMatch = Components.interfaces.nsIAccessibleStates[stateName];
let state = {};
let extState = {};
acc.getState(state, extState);
return !!(state.value & stateToMatch);
},
click: function Accessibility_click(element) {
this._getAccessible(element.wrappedJSObject, function(acc) {
acc.doAction(0);
marionetteScriptFinished();
});
},
wheel: function Accessibility_wheel(element, direction) {
let horizontal = direction === 'left' || direction === 'right';
let page = (direction === 'left' || direction === 'up') ? 1 : -1;
let event = new window.wrappedJSObject.WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaX: horizontal ? page : 0,
deltaY: horizontal ? 0 : page,
deltaMode: window.wrappedJSObject.WheelEvent.DOM_DELTA_PAGE,
});
element.wrappedJSObject.dispatchEvent(event);
},
isDisabled: function Accessibility_isDisabled(element) {
this._getAccessible(element.wrappedJSObject, (acc) => {
marionetteScriptFinished(
{ result: this._matchState(acc, 'STATE_UNAVAILABLE') });
});
},
_isAriaHidden: function Accessibility__isAriaHidden(element) {
do {
if (JSON.parse(element.getAttribute('aria-hidden'))) {
return true;
}
element = element.parentNode;
} while (element && element.getAttribute);
},
isHidden: function Accessibility_isHidden(element) {
let elem = element.wrappedJSObject;
if (this._isAriaHidden(elem)) {
marionetteScriptFinished({ result: true });
return;
}
this._getAccessible(elem, (acc) => {
if (!acc) {
marionetteScriptFinished({ result: true });
return;
}
marionetteScriptFinished(
{ result: this._matchState(acc, 'STATE_INVISIBLE') });
}, true);
},
isVisible: function Accessibility_isVisible(element) {
let elem = element.wrappedJSObject;
if (this._isAriaHidden(elem)) {
marionetteScriptFinished({ result: false });
return;
}
this._getAccessible(elem, (acc) => {
marionetteScriptFinished(
{ result: !this._matchState(acc, 'STATE_INVISIBLE') });
});
},
getName: function Accessibility_getName(element) {
this._getAccessible(element.wrappedJSObject, (acc) => {
marionetteScriptFinished({ result: acc.name });
});
},
getRole: function Accessibility_getRole(element) {
this._getAccessible(element.wrappedJSObject, (acc) => {
marionetteScriptFinished(
{ result: this._accRetrieval.getStringRole(acc.role) });
});
},
};

441
gaia/tests/atoms/gaia_apps.js Executable file
View file

@ -0,0 +1,441 @@
/* 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';
/* globals marionetteScriptFinished, waitFor */
var GaiaApps = {
normalizeName: function(name) {
return name.replace(/[- ]+/g, '').toLowerCase();
},
getInstalledApps: function() {
let req = navigator.mozApps.mgmt.getAll();
req.onsuccess = function() {
marionetteScriptFinished(req.result);
};
},
getRunningApps: function(includeSystemApps) {
let runningApps = GaiaApps.getApps(includeSystemApps);
// Return a simplified version of the runningApps object which can be
// JSON-serialized.
let apps = {};
for (let app in runningApps) {
let anApp = {};
for (let key in runningApps[app]) {
if (['name', 'origin', 'manifest'].indexOf(key) > -1) {
anApp[key] = runningApps[app][key];
}
}
apps[runningApps[app].origin] = anApp;
}
return apps;
},
getApps: function(includeSystemApps) {
let service = window.wrappedJSObject.Service;
let apps = includeSystemApps ?
service.query('getApps') :
service.query('snapshot');
return apps;
},
getRunningAppOrigin: function(name) {
let apps = GaiaApps.getApps(true);
for (let id in apps) {
if (apps[id].name == name) {
return apps[id].origin;
}
}
return undefined;
},
getPermission: function(appName, permissionName) {
GaiaApps.locateWithName(appName, function(app) {
console.log('Getting permission \'' +
permissionName +
'\' for ' +
appName);
var mozPerms = navigator.mozPermissionSettings;
var result = mozPerms.get(
permissionName, app.manifestURL, app.origin, false
);
marionetteScriptFinished(result);
});
},
setPermission: function(appName, permissionName, value) {
GaiaApps.locateWithName(appName, function(app) {
console.log('Setting permission \'' + permissionName + '\' for ' +
appName + 'to \'' + value + '\'');
var mozPerms = navigator.mozPermissionSettings;
mozPerms.set(
permissionName, value, app.manifestURL, app.origin, false
);
marionetteScriptFinished();
});
},
setPermissionByUrl: function(manifestUrl, permissionName, value, entryPoint) {
GaiaApps.locateWithManifestURL(manifestUrl, entryPoint, function(app) {
console.log('Setting permission \'' + permissionName + '\' for ' +
manifestUrl + 'to \'' + value + '\'');
var mozPerms = navigator.mozPermissionSettings;
mozPerms.set(
permissionName, value, app.manifestURL, app.origin, false
);
marionetteScriptFinished();
});
},
sendLocateResponse:
function(aCallback, app, appName, launchPath, entryPoint) {
var callback = aCallback || marionetteScriptFinished;
if (callback === marionetteScriptFinished) {
var result = false;
if (typeof(app) === 'object') {
result = {
name: app.manifest.name,
origin: app.origin,
entryPoint: entryPoint || null,
normalizedName: appName
};
}
callback(result);
} else {
callback(app, appName, launchPath, entryPoint);
}
},
locateWithName: function(name, aCallback) {
var callback = aCallback || marionetteScriptFinished;
let apps = window.wrappedJSObject.applications ||
window.wrappedJSObject.Applications;
let installedApps = apps.installedApps;
let normalizedSearchName = GaiaApps.normalizeName(name);
for (let manifestURL in installedApps) {
let app = installedApps[manifestURL];
let entryPoints = app.manifest.entry_points;
if (entryPoints) {
for (let ep in entryPoints) {
let currentEntryPoint = entryPoints[ep];
let appName = currentEntryPoint.name;
let launchPath = currentEntryPoint.launch_path;
let locales = currentEntryPoint.locales;
if (normalizedSearchName === GaiaApps.normalizeName(appName)) {
return GaiaApps.sendLocateResponse(callback,
app,
appName,
launchPath,
ep);
} else if (locales) {
for (let id in locales) {
let localisedAppName = locales[id].name;
if (localisedAppName && normalizedSearchName ===
GaiaApps.normalizeName(localisedAppName)) {
return GaiaApps.sendLocateResponse(callback,
app,
appName,
launchPath,
ep);
}
}
}
}
} else {
let appName = app.manifest.name;
let launchPath = app.manifest.launch_path;
let locales = app.manifest.locales;
if (normalizedSearchName === GaiaApps.normalizeName(appName)) {
return GaiaApps.sendLocateResponse(callback,
app,
appName,
launchPath);
} else if (locales) {
for (let id in locales) {
let localisedAppName = locales[id].name;
if (localisedAppName && normalizedSearchName ===
GaiaApps.normalizeName(localisedAppName)) {
return GaiaApps.sendLocateResponse(callback,
app,
appName,
launchPath);
}
}
}
}
}
callback(false);
},
locateWithManifestURL: function(manifestURL, entryPoint, aCallback) {
var callback = aCallback || marionetteScriptFinished;
var apps = window.wrappedJSObject.applications ||
window.wrappedJSObject.Applications;
var app = apps.getByManifestURL(manifestURL);
var launchPath;
if (app != null) {
GaiaApps._formatApp(app, manifestURL, entryPoint, callback, launchPath);
} else {
callback(false);
}
},
_formatApp: function(app, manifestURL, entryPoint, callback, launchPath) {
if (entryPoint) {
if (app.manifest.entry_points[entryPoint]) {
launchPath = app.manifest.entry_points[entryPoint].launch_path;
} else {
app = null;
}
} else {
launchPath = app.manifest.launch_path;
}
GaiaApps.sendLocateResponse(callback, app,
manifestURL, launchPath, entryPoint);
},
// Returns the number of running apps.
// if includeSystemApps is true then system always-running
// apps (eg Homescreen) will be counted
numRunningApps: function(includeSystemApps) {
return Object.keys(GaiaApps.getApps(includeSystemApps)).length;
},
isRunning: function(origin) {
var apps = GaiaApps.getApps(true);
for (var id in apps) {
if (apps[id].origin === origin) {
return true;
}
}
return false;
},
// Kills the specified app.
kill: function(aOrigin, aCallback) {
var callback = aCallback || marionetteScriptFinished;
if (!GaiaApps.isRunning(aOrigin)) {
callback(false);
}
else {
window.addEventListener('appterminated', function gt_onAppTerminated() {
window.removeEventListener('appterminated', gt_onAppTerminated);
waitFor(
function() {
console.log('app with origin \'' + aOrigin + '\' has terminated');
callback(true);
},
function() {
return !GaiaApps.isRunning(aOrigin);
}
);
});
console.log('terminating app with origin \'' + aOrigin + '\'');
window.wrappedJSObject.Service.request('AppWindowManager:kill', aOrigin);
}
},
// Kills all running apps that may be killed by the user, defined as
// being accessible by the Cards View/Stack Manager
killAll: function() {
let originsToClose = [];
let that = this;
let apps = GaiaApps.getApps();
for (let id in apps) {
let origin = apps[id].origin;
originsToClose.push(origin);
}
if (!originsToClose.length) {
marionetteScriptFinished(true);
return;
}
originsToClose.slice(0).forEach(function(origin) {
GaiaApps.kill(origin, function() {});
});
// Even after the 'appterminated' event has been fired for an app,
// it can still exist in the apps list, so wait until 1 or fewer
// apps are running (since we don't close the homescreen app).
waitFor(
function() { marionetteScriptFinished(true); },
function() { return that.numRunningApps() <= 1; }
);
},
launch: function(app, appName, launchPath, entryPoint) {
if (app) {
let manifestURL = app.manifestURL;
let origin = app.origin;
let sendResponse = function() {
let result = GaiaApps.getDisplayedApp();
marionetteScriptFinished(result);
};
let displayedApp = GaiaApps.getDisplayedApp();
if ((displayedApp.manifestURL == manifestURL &&
displayedApp.entryPoint == entryPoint) ||
(!displayedApp.manifestURL && displayedApp.origin == origin)) {
console.log(`app with origin: ${displayedApp.origin} and
manifestURL: ${displayedApp.manifestURL} and
entryPoint: ${displayedApp.entryPoint} is already running`);
sendResponse();
} else {
window.addEventListener('windowopened', function appOpen() {
window.removeEventListener('windowopened', appOpen);
waitFor(
function() {
console.log(`app with origin: ${displayedApp.origin} and
manifestURL: ${displayedApp.manifestURL} and
entryPoint: ${displayedApp.entryPoint} has launched`);
sendResponse();
},
function() {
// wait for the displayed app to have expected manifestURL/origin
let displayedApp = GaiaApps.getDisplayedApp();
if (displayedApp.manifestURL) {
return displayedApp.manifestURL == manifestURL &&
displayedApp.entryPoint == entryPoint;
} else {
return displayedApp.origin == origin;
}
}
);
});
console.log('launching app with name \'' + appName + '\'');
app.launch(entryPoint || null);
}
} else {
marionetteScriptFinished(false);
}
},
// Launches app with the specified name (e.g., 'Calculator'); returns the
// an object with the app frame if successful, false if the app can't be
// found, or times out if the app frame can't be found after launching the
// app.
launchWithName: function(name) {
GaiaApps.locateWithName(name, this.launch.bind(this));
},
// Launches app with the specified manifestURL. returns the
// an object with the app frame if successful, false if the app can't be
// found, or times out if the app frame can't be found after launching the
// app.
//
// This is prefered over launchWithName because localized builds have
// different names
launchWithManifestURL: function(manifestURL, entryPoint) {
GaiaApps.locateWithManifestURL(manifestURL, entryPoint, this.launch);
},
close: function(app, appName, entryPoint) {
if (app) {
let origin = GaiaApps.getRunningAppOrigin(appName);
GaiaApps.kill(origin);
} else {
marionetteScriptFinished(false);
}
},
// Closes app with the specified name (e.g., 'Calculator'); returns nothing
closeWithName: function(name) {
GaiaApps.locateWithName(name, this.close);
},
closeWithManifestURL: function(manifestURL, entryPoint) {
GaiaApps.locateWithManifestURL(manifestURL, entryPoint, this.close);
},
/**
* Returns the currently displayed app.
* This is more like getDisplayedFrame really,
* as we return what frame the user is interacting with
*/
getDisplayedApp: function() {
let app = window.wrappedJSObject.Service.query('getTopMostWindow');
if (!app || app.isLockscreen) {
// fallback to app window.
app = window.wrappedJSObject.Service
.query('AppWindowManager.getActiveWindow').getTopMostWindow();
}
let currentEntryPoint = null;
// When browser is on a website, app.manifest is null
if (app.manifest) {
let entryPoints = app.manifest.entry_points;
if (entryPoints) {
for (let ep in entryPoints) {
if (entryPoints[ep].launch_path == app.manifest.launch_path) {
currentEntryPoint = ep;
break;
}
}
}
}
let result = {
frame: (app.browser) ? app.browser.element : app.frame.firstChild,
src: (app.browser) ? app.browser.element.src : app.iframe.src,
name: app.name,
origin: app.origin,
manifestURL: app.manifestURL,
entryPoint: currentEntryPoint
};
return result;
},
/**
* Install the app with the specified ManifestURL
*/
install: function(manifestURL) {
let req = navigator.mozApps.install(manifestURL);
req.onsuccess = function() {
marionetteScriptFinished(true);
};
req.onerror = function() {
marionetteScriptFinished(req.error);
};
},
installPackage: function(manifestURL) {
let req = navigator.mozApps.installPackage(manifestURL);
req.onsuccess = function() {
marionetteScriptFinished(true);
};
req.onerror = function() {
marionetteScriptFinished(req.error);
};
},
/**
* Uninstall the app with the specified ManifestURL
*/
uninstall: function(manifestURL) {
GaiaApps.locateWithManifestURL(manifestURL, null, function uninstall(app) {
if (typeof(app) !== 'object') {
// App is already not there, so nothing to do here
marionetteScriptFinished(true);
return;
}
let req = navigator.mozApps.mgmt.uninstall(app);
req.onsuccess = function() {
marionetteScriptFinished(true);
};
req.onerror = function() {
marionetteScriptFinished(req.error);
};
});
}
};

View file

@ -0,0 +1,621 @@
/* 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';
/* global marionetteScriptFinished, pair, mozContact, i */
/* global waitFor, aContacts */
/* exported pair, discovery, GaiaDataLayer */
/* jshint -W083 */
var GaiaDataLayer = {
getBluetoothDefaultAdapter: function() {
var bluetooth = window.navigator.mozBluetooth;
if (bluetooth.defaultAdapter) {
return bluetooth.defaultAdapter;
} else {
return bluetooth.getAdapters()[0];
}
},
setBluetooth: function(aState) {
var adapter = this.getBluetoothDefaultAdapter();
if (adapter.state == aState) {
console.log('bluetooth already ' + aState);
marionetteScriptFinished(true);
return;
}
waitFor(
function() {
console.log('bluetooth ' + aState);
marionetteScriptFinished(true);
},
function() {
console.log('bluetooth enable status: ' + adapter.state);
// Wait for the adapter state to become 'disabled' or 'enabled'
if (adapter.state === 'disabling' || adapter.state === 'enabling') {
return false;
}
if (adapter.state != aState) {
console.log('trying to make bluetooth ' + aState);
if (aState === 'enabled') {
adapter.enable();
} else {
adapter.disable();
}
return false;
}
return adapter.state === aState;
}
);
},
disableBluetooth: function() {
this.setBluetooth('disabled');
},
enableBluetooth: function() {
this.setBluetooth('enabled');
},
insertContact: function(aContact) {
// requires the 'contacts-create' permission
if (aContact.photo) {
var blob = GaiaDataLayer.base64ToBlob(aContact.photo, 'image/jpg');
aContact.photo = [blob];
}
var contact = new mozContact(aContact);
var req = window.navigator.mozContacts.save(contact);
req.onsuccess = function() {
console.log('success saving contact');
marionetteScriptFinished(true);
};
req.onerror = function() {
console.error('error saving contact', req.error.name);
marionetteScriptFinished(false);
};
},
insertSIMContact: function(aType, aContact) {
// Get 1st SIM
var iccId = window.navigator.mozIccManager.iccIds[0];
var icc = window.navigator.mozIccManager.getIccById(iccId);
var simContact = new window.mozContact(aContact);
if ('id' in aContact) {
simContact.id = aContact.id;
}
var req = icc.updateContact(aType, simContact);
req.onsuccess = function() {
console.log('success saving contact to SIM');
marionetteScriptFinished(req.result);
};
req.onerror = function() {
console.error('error saving contact to SIM', req.error.name);
marionetteScriptFinished(false);
};
},
deleteSIMContact: function(aType, aId) {
// Get 1st SIM
var iccId = window.navigator.mozIccManager.iccIds[0];
var icc = window.navigator.mozIccManager.getIccById(iccId);
var aContact = new mozContact();
aContact.id = aId;
var req = icc.updateContact(aType, aContact);
req.onsuccess = function() {
console.log('success removing contact from SIM');
marionetteScriptFinished(true);
};
req.onerror = function() {
console.error('error removing contact from SIM', req.error.name);
marionetteScriptFinished(false);
};
},
getAllContacts: function(aCallback) {
// requires 'contacts-read' permission
var callback = aCallback || marionetteScriptFinished;
var req = window.navigator.mozContacts.find({});
req.onsuccess = function() {
console.log('success finding contacts');
callback(req.result);
};
req.onerror = function() {
console.error('error finding contacts ' + req.error.name);
callback([]);
};
},
getSIMContacts: function(aType, aCallback) {
var type = aType || 'adn';
var callback = aCallback || marionetteScriptFinished;
var icc = navigator.mozIccManager;
// See bug 932134
// To keep all tests passed while introducing multi-sim APIs, in bug 928325
// we do the following check. Remove it after the APIs land.
if (icc && icc.iccIds && icc.iccIds[0]) {
icc = icc.getIccById(icc.iccIds[0]);
}
var req = icc.readContacts(type);
req.onsuccess = function() {
console.log('success finding ' + type + ' contacts');
callback(req.result);
};
req.onerror = function() {
console.error('error finding ' + type + ' contacts ' + req.error.name);
callback([]);
};
},
removeAllContacts: function() {
var self = this;
this.getAllContacts(function(aContacts) {
if (aContacts.length > 0) {
var contactsLength = aContacts.length;
var done = 0;
for (var i = 0; i < contactsLength; i++) {
self.removeContact(aContacts[i], function() {
if (++done === contactsLength) {
marionetteScriptFinished(true);
}
});
}
}
else {
console.log('no contacts to remove');
marionetteScriptFinished(true);
}
});
},
removeContact: function(aContact, aCallback) {
// requires the 'contacts-write' persmission
var callback = aCallback || marionetteScriptFinished;
console.log('removing contact with id \'' + aContact.id + '\'');
var req = window.navigator.mozContacts.remove(aContact);
req.onsuccess = function() {
console.log('success removing contact with id \'' + aContact.id + '\'');
callback(true);
};
req.onerror = function() {
console.error('error removing contact with id \'' +
aContacts[i].id + '\'');
callback(false);
};
},
getSetting: function(aName, aCallback) {
// requires the 'settings-read' and 'settings-api-read' permissions
var callback = aCallback || marionetteScriptFinished;
var req = window.navigator.mozSettings.createLock().get(aName);
req.onsuccess = function() {
console.log('setting retrieved');
let result = aName === '*' ? req.result : req.result[aName];
callback(result);
};
req.onerror = function() {
console.log('error getting setting ' + req.error.name);
};
},
setSetting: function(aName, aValue, aReturnOnSuccess) {
// requires the 'settings-write' and 'settings-api-write' permissions
var returnOnSuccess = aReturnOnSuccess || aReturnOnSuccess === undefined;
var setting = {};
setting[aName] = aValue;
console.log('setting ' + aName + ' to ' + aValue);
var lock = window.navigator.mozSettings.createLock();
var req = lock.set(setting);
lock.onsettingstransactionsuccess = function() {
console.log('setting changed');
if (returnOnSuccess) {
marionetteScriptFinished(true);
}
};
lock.onsettingstransactionfailure = function() {
console.log('error changing setting ' + req.error.name);
marionetteScriptFinished(false);
};
},
connectToWiFi: function(aNetwork, aCallback) {
var callback = aCallback || marionetteScriptFinished;
var manager = window.navigator.mozWifiManager;
if (this.isWiFiConnected(aNetwork)) {
console.log('already connected to network with ssid \'' +
aNetwork.ssid + '\'');
callback(true);
}
else {
var req;
if (window.MozWifiNetwork === undefined) {
req = manager.associate(aNetwork);
} else {
req = manager.associate(new window.MozWifiNetwork(aNetwork));
}
req.onsuccess = function() {
console.log('waiting for connection status \'connected\'');
waitFor(
function() {
console.log('success connecting to network with ssid \'' +
aNetwork.ssid + '\'');
callback(true);
},
function() {
console.log('connection status: ' + manager.connection.status);
return manager.connection.status === 'connected';
}
);
};
req.onerror = function() {
console.log('error connecting to network ' + req.error.name);
callback(false);
};
}
},
disableWiFi: function() {
var manager = window.navigator.mozWifiManager;
if (manager.enabled) {
waitFor(
function() { marionetteScriptFinished(true); },
function() {
console.log('wifi enabled status: ' + manager.enabled);
return manager.enabled === false;
});
this.setSetting('wifi.enabled', false, false);
}
else {
console.log('wifi already disabled');
marionetteScriptFinished(true);
}
},
enableWiFi: function() {
var manager = window.navigator.mozWifiManager;
if (!manager.enabled) {
waitFor(
function() { marionetteScriptFinished(true); },
function() {
console.log('wifi enabled status: ' + manager.enabled);
return manager.enabled === true;
});
this.setSetting('wifi.enabled', true, false);
}
else {
console.log('wifi already enabled');
marionetteScriptFinished(true);
}
},
forgetAllNetworks: function(aCallback) {
var callback = aCallback || marionetteScriptFinished;
var self = this;
this.getKnownNetworks(function(aNetworks) {
if (aNetworks.length > 0) {
var networksLength = aNetworks.length;
var done = 0;
for (var i = 0; i < networksLength; i++) {
self.forgetWiFi(aNetworks[i], function() {
if (++done === networksLength) {
callback(true);
}
}, false);
}
}
else {
console.log('no known networks to forget');
callback(true);
}
});
},
getKnownNetworks: function(aCallback) {
var callback = aCallback || marionetteScriptFinished;
var manager = window.navigator.mozWifiManager;
var req = manager.getKnownNetworks();
req.onsuccess = function() {
console.log('success getting known networks');
callback(req.result);
};
req.onerror = function() {
console.log('error getting known networks ' + req.error.name);
callback([]);
};
},
forgetWiFi: function(aNetwork, aCallback, aWaitForStatus) {
var callback = aCallback || marionetteScriptFinished;
var waitForStatus = aWaitForStatus || 'disconnected';
var manager = window.navigator.mozWifiManager;
var req = manager.forget(aNetwork);
req.onsuccess = function() {
console.log('success forgetting network with ssid \'' +
aNetwork.ssid + '\'');
if (waitForStatus !== false) {
console.log('waiting for connection status \'' +
waitForStatus + '\'');
waitFor(
function() { callback(true); },
function() {
console.log('connection status: ' + manager.connection.status);
return manager.connection.status === waitForStatus;
}
);
}
else {
callback(true);
}
};
req.onerror = function() {
console.log('error forgetting network with ssid \'' +
aNetwork.ssid + '\' ' + req.error.name);
callback(false);
};
},
isWiFiConnected: function(aNetwork) {
let manager = window.navigator.mozWifiManager;
let connected = manager.connection.status === 'connected';
if (connected && aNetwork) {
return manager.connection.network.ssid === aNetwork.ssid;
} else {
return connected;
}
},
getMozTelephonyState: function() {
return window.navigator.mozTelephony.active.state;
},
connectToCellData: function() {
var manager = window.navigator.mozMobileConnections &&
window.navigator.mozMobileConnections[0];
if (!manager.data.connected) {
waitFor(
function() {
console.log('cell data enabled');
marionetteScriptFinished(true);
},
function() { return manager.data.connected; }
);
this.setSetting('ril.data.enabled', true, false);
}
else {
console.log('cell data already connected');
marionetteScriptFinished(true);
}
},
disableCellData: function() {
var self = this;
this.getSetting('ril.data.enabled', function(aCellDataEnabled) {
var manager = window.navigator.mozMobileConnections &&
window.navigator.mozMobileConnections[0];
if (aCellDataEnabled) {
waitFor(
function() {
console.log('cell data disabled');
marionetteScriptFinished(true);
},
function() { return !manager.data.connected; }
);
self.setSetting('ril.data.enabled', false, false);
}
else {
console.log('cell data already disabled');
marionetteScriptFinished(true);
}
});
},
getAllPictures: function() {
this.getFiles('pictures');
},
getAllVideos: function() {
this.getFiles('videos');
},
getAllMusic: function() {
this.getFiles('music');
},
getAllSDCardFiles: function() {
this.getFiles('sdcard');
},
getFiles: function(aType, aCallback) {
var callback = aCallback || marionetteScriptFinished;
var files = [];
console.log('getting ' + aType);
var storage = navigator.getDeviceStorage(aType);
var req = storage.enumerate();
req.onsuccess = function() {
var file = req.result;
if (file) {
if (aType === 'music' && file.name.slice(-4) === '.3gp') {
// 3gp is both music and video; we skip the music definition
req.continue();
}
else {
// File.name returns a fully qualified path
files.push({'name': file.name, 'size': file.size});
req.continue();
}
}
else {
callback(files);
}
};
req.onerror = function() {
console.error('failed to enumerate ' + aType + ' ' + req.error.name);
callback(false);
};
},
sendSMS: function(recipient, content, skipVerification, aCallback) {
// requires the 'sms' permission and the 'dom.sms.enabled' pref
var callback = aCallback || marionetteScriptFinished;
console.log('sending sms message to number: ' + recipient);
let messageManager = window.navigator.mozMobileMessage;
let request = messageManager.send(recipient, content);
if (skipVerification) {
marionetteScriptFinished(true);
return;
}
request.onsuccess = function(event) {
var sms = event.target.result;
waitFor(
function() { callback(true); },
function() {
console.log('sms delivery state: ' + sms.delivery);
return sms.delivery === 'sent';
}
);
};
request.onerror = function() {
console.log('sms message not sent');
callback(false);
};
},
getAllSms: function(aCallback) {
// requires the 'sms' permission and the 'dom.sms.enabled' pref
var callback = aCallback || marionetteScriptFinished;
console.log('searching for sms messages');
let sms = window.navigator.mozMobileMessage;
let msgList = [];
let cursor = sms.getMessages(null, false);
cursor.onsuccess = function(event) {
if(cursor.result) {
// Add the sms to the list
msgList.push(cursor.result);
// Now get the next in the list
cursor.continue();
}else{
// Send back the list
callback(msgList);
}
};
cursor.onerror = function(event) {
console.log('sms.getMessages error: ' + event.target.error.name);
callback(false);
};
},
deleteAllSms: function(aCallback) {
// requires the 'sms' permission and the 'dom.sms.enabled' pref
var callback = aCallback || marionetteScriptFinished;
console.log('searching for sms messages');
let sms = window.navigator.mozMobileMessage;
let msgList = [];
let cursor = sms.getMessages(null, false);
cursor.onsuccess = function(event) {
// Check if message was found
if (cursor.result) {
msgList.push(cursor.result.id);
// Now get next message in the list
cursor.continue();
} else {
// No (more) messages found
if (msgList.length) {
console.log('found ' + msgList.length + ' sms messages to delete');
deleteSmsMsgs(msgList);
} else {
console.log('zero sms messages found');
callback(true);
}
}
};
cursor.onerror = function(event) {
console.log('sms.getMessages error: ' + event.target.error.name);
callback(false);
};
function deleteSmsMsgs(msgList) {
let smsId = msgList.shift();
console.log('deleting sms id: ' + smsId);
let request = sms.delete(smsId);
request.onsuccess = function(event) {
if (event.target.result) {
// Message deleted, continue until none are left
if (msgList.length) {
deleteSmsMsgs(msgList);
} else {
// All messages deleted
console.log('finished deleting all sms messages');
callback(true);
}
} else {
console.log('sms delete failed');
callback(false);
}
};
request.onerror = function(event) {
console.log('sms.delete request returned unexpected error: ' +
event.target.error.name);
callback(false);
};
}
},
deleteAllAlarms: function() {
window.wrappedJSObject.AlarmManager.getAlarmList(function(aList) {
aList.forEach(function(aAlarm) {
console.log('Deleting alarm with id \'' + aAlarm.id + '\'');
window.wrappedJSObject.AlarmManager.delete(aAlarm);
});
});
},
base64ToBlob: function(base64, mimeType) {
var binary = atob(base64);
var len = binary.length;
var buffer = new ArrayBuffer(len);
var view = new Uint8Array(buffer);
for (var i = 0; i < len; i++) {
view[i] = binary.charCodeAt(i);
}
return new Blob([view], {type: mimeType});
}
};

View file

@ -0,0 +1,57 @@
/* 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';
/* globals waitFor, finish */
/* exported GaiaLockScreen */
var GaiaLockScreen = {
unlock: function(forcibly) {
let setlock = window.wrappedJSObject.SettingsListener.getSettingsLock();
let service = window.wrappedJSObject.Service;
let obj = {'screen.timeout': 0};
setlock.set(obj);
waitFor(
function() {
service.request('unlock', { forcibly: forcibly });
waitFor(
function() {
finish(service.locked);
},
function() {
return !service.locked;
}
);
},
function() {
return !!service;
}
);
},
lock: function(forcibly) {
let service = window.wrappedJSObject.Service;
let setlock = window.wrappedJSObject.SettingsListener.getSettingsLock();
let obj = {'screen.timeout': 0};
setlock.set(obj);
waitFor(
function() {
service.request('lock', { forcibly: forcibly });
waitFor(
function() {
finish(!service.locked);
},
function() {
return service.locked;
}
);
},
function() {
return !!service;
}
);
}
};

37
gaia/tests/atoms/reflow.js Executable file
View file

@ -0,0 +1,37 @@
'use strict';
(function(exports) {
if ('MozReflowAtom' in exports) {
return;
}
exports.MozReflowAtom = {
count: null,
_trackingManifest: null,
init: function rw_init() {
exports.addEventListener('developer-hud-update', this);
},
handleEvent: function rw_handleEvent(evt) {
var metric = evt.detail.metric.name || evt.detail.metric;
if (metric !== 'reflows' ||
evt.detail.manifest !== this._trackingManifest) {
return;
}
this.count++;
},
startTracking: function(manifestURL) {
this._trackingManifest = manifestURL;
this.count = 0;
},
stopTracking: function() {
this.count = 0;
this._trackingManifest = null;
},
};
exports.MozReflowAtom.init();
})(window);

15
gaia/tests/atoms/remote_date.js Executable file
View file

@ -0,0 +1,15 @@
return (function(ms) {
var date = new Date();
return {
year: date.getFullYear(),
month: date.getMonth(),
date: date.getDate(),
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds()
};
}.apply(this, arguments));

20
gaia/tests/atoms/screenshot.js Executable file
View file

@ -0,0 +1,20 @@
return (function takeScreenshot() {
var canvas = document.createElementNS('http://www.w3.org/1999/xhtml',
'canvas');
var width = window.innerWidth;
var height = window.innerHeight;
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
var context = canvas.getContext('2d');
var flags =
context.DRAWWINDOW_DRAW_CARET |
context.DRAWWINDOW_DRAW_VIEW |
context.DRAWWINDOW_USE_WIDGET_LAYERS;
context.drawWindow(window, 0, 0, width, height,
'rgb(255,255,255)', flags);
return context.canvas.toDataURL('image/png');
}.apply(this, arguments));

View file

@ -0,0 +1,3 @@
#! /bin/bash -e
make build-test-integration

View file

@ -0,0 +1,3 @@
#! /bin/bash -e
make build-test-unit

3
gaia/tests/ci/csslint/script Executable file
View file

@ -0,0 +1,3 @@
#! /bin/bash -vex
make csslint

3
gaia/tests/ci/eslint/script Executable file
View file

@ -0,0 +1,3 @@
#! /bin/bash -vex
make eslint

View file

@ -0,0 +1,11 @@
#! /bin/bash -vex
mkdir -p artifacts/logs/gaia_ui_tests_gecko/
./bin/gaia-ui-tests \
--type=b2g-external \
--gecko-log artifacts/gaia_ui_tests_gecko/ \
--html-output artifacts/gaia_ui_tests_report.html \
--this-chunk=${THIS_CHUNK:=1} \
--total-chunks=${TOTAL_CHUNKS:=1} \
tests/python/gaia-ui-tests/gaiatest/tests/$SUITE/manifest.ini
ls -lah artifacts

3
gaia/tests/ci/jshint/script Executable file
View file

@ -0,0 +1,3 @@
#! /bin/bash -vex
make hint

View file

@ -0,0 +1,3 @@
#! /bin/bash -vex
make jsmarionette-unit-tests

3
gaia/tests/ci/jsonlint/script Executable file
View file

@ -0,0 +1,3 @@
#! /bin/bash -vex
make jsonlint

2
gaia/tests/ci/linters/install Executable file
View file

@ -0,0 +1,2 @@
#! /bin/bash -ve
easy_install http://closure-linter.googlecode.com/files/closure_linter-latest.tar.gz

2
gaia/tests/ci/linters/script Executable file
View file

@ -0,0 +1,2 @@
#! /bin/bash -v
make lint

View file

@ -0,0 +1,7 @@
#! /bin/bash -ve
echo "Downloading Mulet"
make mulet
# Install virtualenv and radicale on Travis.
make caldav-server-install

View file

@ -0,0 +1,3 @@
#! /bin/bash -vex
make test-integration

View file

@ -0,0 +1,10 @@
#! /bin/bash -ve
echo "Downloading Mulet"
make mulet
# Install virtualenv and radicale on Travis.
make caldav-server-install
echo "Building test profile"
PROFILE_FOLDER=profile-test make

View file

@ -0,0 +1,75 @@
'use strict';
/* global require, process */
var fs = require('fs'),
request = require('request');
var PULL_REQUEST_URL_PATTERN =
'https://api.github.com/repos/mozilla-b2g/gaia/pulls/[id]/files',
MARIONETTE_TEST_FILE_NAME_PATTERN =
/apps\/[a-z]+\/test\/marionette\/[\w\/]+_test.js/;
/**
* Get file name list of marionette test in the current pull request on Travis.
*/
var PullRequestMarionetteTest = (function() {
/**
* Get files in a pull request.
*
* @param {String|Number} id The pull request id.
* @param {Function} callback Function with the pull request files array.
*/
function getPullRequestFiles(id, callback) {
// Configure the request
var options = {
url: PULL_REQUEST_URL_PATTERN.replace('[id]', id),
method: 'GET',
headers: { 'User-Agent': 'FirefoxOS-Gaia-Travis' }
};
request(options, function(error, response, body) {
if (!error && response.statusCode == 200) {
var json = JSON.parse(body),
fileNames = [];
json.forEach(function(file) {
fileNames.push(file.filename);
});
if (callback && typeof(callback) === 'function') {
callback(fileNames);
}
} else {
throw new Error('Cannot access GitHub API.');
}
});
}
// Main function.
function execute() {
// Pull request ID for the current Travis job.
var pullRequestId = process.env.TRAVIS_PULL_REQUEST;
getPullRequestFiles(pullRequestId, function(fileNames) {
var testFileNames = [],
returnString = '';
testFileNames = fileNames.filter(function(filename) {
// Get the marionette test file list in the patch.
if (filename.match(MARIONETTE_TEST_FILE_NAME_PATTERN) &&
fs.existsSync(filename)) {
return true;
}
});
if (testFileNames.length > 0) {
returnString = testFileNames.toString().split(',').join(' ');
}
console.log(returnString);
});
}
return {
execute: execute
};
})();
PullRequestMarionetteTest.execute();

View file

@ -0,0 +1,33 @@
#! /bin/bash -v
###############################################################################
# For having stable marionette tests, #
# we run specfic marionette tests for 30 times on Travis. #
###############################################################################
MARIONETTE_TEST_FILE_NAMES=$(node tests/ci/marionette_js_stable_check/pull-request-marionette-test)
if [ ${#MARIONETTE_TEST_FILE_NAMES} -gt 0 ]; then
TEST_RESULT=0
MAKE_BUILD="PROFILE_FOLDER=profile-test make"
RUN_SPECIFIC_TESTS="make test-integration-test TEST_FILES=\"$MARIONETTE_TEST_FILE_NAMES\" REPORTER=spec"
# We only build once
eval ${MAKE_BUILD}
# Then we would run tests for six times
for i in {1..6}
do
eval ${RUN_SPECIFIC_TESTS}
if [ $? -ne 0 ]; then
TEST_RESULT=1
fi
done
if [ $TEST_RESULT -eq 1 ]; then
echo "The marionette tests are not passed for 6 times continually."
exit 1
fi
else
echo "The pull request has no marionette test to run."
fi

View file

@ -0,0 +1,5 @@
#! /bin/bash -ve
gem install travis-artifacts
travis-artifacts upload --path artifacts/
echo "Uploaded to:"
echo "http://$ARTIFACTS_S3_BUCKET.s3.amazonaws.com/artifacts/$TRAVIS_BUILD_NUMBER/$TRAVIS_JOB_NUMBER/iupload.txt$"

4
gaia/tests/ci/s3upload/script Executable file
View file

@ -0,0 +1,4 @@
#! /bin/bash -e
mkdir -p artifacts
echo "upload me" > artifacts/iupload.txt

View file

@ -0,0 +1,33 @@
#! /bin/bash -v
# not using -e here because we use code failure for other things here.
RETRY=30
myself=`basename $0`
log() {
echo "LOG $myself: $@"
}
waiting_port() {
log "Waiting for embedded httpd server on port $1..."
for i in $(seq 1 $RETRY); do
nc -z localhost $1
if [ $? -eq 0 ]; then
log "Success!"
return
fi
log "Waited $i seconds."
sleep 1
done
log "Failed :("
exit 1
}
echo "Starting test-agent-server"
make test-agent-server &
echo 'Starting Firefox'
FIREFOX=`find firefox -name "firefox" | tail -n 1`
$FIREFOX -profile `pwd`/profile-debug "http://test-agent.gaiamobile.org:8080" > /dev/null 2>&1 &
waiting_port 8080

View file

@ -0,0 +1,12 @@
#! /bin/bash -ve
echo 'Downloading Firefox'
./node_modules/.bin/mozilla-download --product firefox \
--channel tinderbox \
--branch mozilla-central \
$PWD/firefox
echo 'Downloading & installing node dependencies'
make common-install
echo 'Downloading xulrunner-sdk and making profile for testing (more than 5 minutes)'
DEBUG=1 DESKTOP=0 WGET_OPTS=-nv make

View file

@ -0,0 +1,3 @@
#! /bin/bash -v
make test-agent-test REPORTER=Min COVERAGE=1

View file

@ -0,0 +1,45 @@
#! /bin/bash -v
# not using -e here because we use code failure for other things here.
RETRY=30
myself=`basename $0`
log() {
echo "LOG $myself: $@"
}
waiting_port() {
log "Waiting for embedded httpd server on port $1..."
for i in $(seq 1 $RETRY); do
nc -z localhost $1
if [ $? -eq 0 ]; then
log "Success!"
return
fi
log "Waited $i seconds."
sleep 1
done
log "Failed :("
exit 1
}
make node_modules
echo "Starting test-agent-server"
make test-agent-server &
echo 'Starting Mulet'
MULET_PATH="./firefox/firefox-bin"
if [ ! -x "$MULET_PATH" ] ; then
# set the path for Mac OS X
MULET_PATH="./firefox/Contents/MacOS/firefox-bin"
fi
# if we still don't have it, fail
if [ ! -x "$MULET_PATH" ] ; then
echo "mulet path doesn't exists"
exit 1
fi
$MULET_PATH -profile `pwd`/profile-debug app://test-agent.gaiamobile.org/ &
waiting_port 8080

View file

@ -0,0 +1,9 @@
#! /bin/bash -ve
echo 'Downloading Mulet'
make mulet
echo 'Downloading & installing node dependencies'
make common-install
echo 'Downloading xulrunner-sdk and making profile for testing (more than 5 minutes)'
NO_LOCK_SCREEN=1 DEBUG=1 DESKTOP=0 WGET_OPTS=-nv make

View file

@ -0,0 +1,7 @@
#! /bin/bash -vex
./node_modules/test-agent/bin/js-test-agent test \
--this-chunk=${THIS_CHUNK:=1} \
--total-chunks=${TOTAL_CHUNKS:=1} \
--server ws://localhost:8789 \
--reporter ${REPORTER:=spec}

View file

@ -0,0 +1,33 @@
#! /bin/bash -v
# not using -e here because we use code failure for other things here.
RETRY=30
myself=`basename $0`
log() {
echo "LOG $myself: $@"
}
waiting_port() {
log "Waiting for embedded httpd server on port $1..."
for i in $(seq 1 $RETRY); do
nc -z localhost $1
if [ $? -eq 0 ]; then
log "Success!"
return
fi
log "Waited $i seconds."
sleep 1
done
log "Failed :("
exit 1
}
echo "Starting test-agent-server"
make test-agent-server &
echo 'Starting Firefox'
FIREFOX=`find firefox -name "firefox" | tail -n 1`
$FIREFOX -profile `pwd`/profile-debug "app://test-agent.gaiamobile.org" &
waiting_port 8080

View file

@ -0,0 +1,14 @@
#! /bin/bash -ve
make node_modules
echo 'Downloading Firefox'
DEBUG=* ./node_modules/.bin/mozilla-download --product firefox \
--channel tinderbox \
--branch mozilla-central \
$PWD/firefox
echo 'Downloading & installing node dependencies'
make common-install
echo 'Downloading xulrunner-sdk and making profile for testing (more than 5 minutes)'
DEBUG=1 DESKTOP=0 WGET_OPTS=-nv make

View file

@ -0,0 +1,3 @@
#! /bin/bash -v
make test-agent-test REPORTER=Dot

30
gaia/tests/ci/venv.sh Executable file
View file

@ -0,0 +1,30 @@
#! /bin/bash -e
# using a venv avoids the need for any root weirdness and makes it somewhat easy
# to replicate this setup locally. Original script taken from the gaia-ui-tests
# install.
DIR_TEST_ENV=$PWD/ci_venv
VERSION_VIRTUALENV=1.11
URL_VIRTUALENV=https://pypi.python.org/packages/source/v/virtualenv/virtualenv-${VERSION_VIRTUALENV}.tar.gz
setup_venv() {
echo "creating virtual environment"
if type virtualenv >/dev/null 2>&1; then
virtualenv $DIR_TEST_ENV
else
echo "downloading virtualenv from ${URL_VIRTUALENV}"
curl -O ${URL_VIRTUALENV}
tar xvfz virtualenv-${VERSION_VIRTUALENV}.tar.gz
python virtualenv-${VERSION_VIRTUALENV}/virtualenv.py ${DIR_TEST_ENV}
fi
source $DIR_TEST_ENV/bin/activate
}
if [ "$VIRTUAL_ENV" != "$DIR_TEST_ENV" ]
then
# don't rerun virutal env if already in the right venv
setup_venv
fi

View file

@ -0,0 +1 @@
[server_test.js]

View file

@ -0,0 +1,39 @@
'use strict';
/* global Cu, DebuggerServer, marionette, require, setup, test */
var assert = require('assert');
marionette('Dev Tools server', function() {
var client = marionette.client({
profile: {
prefs: {
'devtools.debugger.prompt-connection': false,
'devtools.debugger.forbid-certified-apps': false,
'devtools.debugger.unix-domain-socket': '6080'
},
settings: {
'debugger.remote-mode': 'adb-devtools'
}
}
});
client = client.scope({ context: 'chrome' });
setup(function() {
var remoteMode = client.settings.get('debugger.remote-mode');
assert.equal(remoteMode, 'adb-devtools', 'Remote mode setting verified');
});
test('is running and listening', function() {
var debuggerServerInited = client.executeScript(function() {
Cu.import('resource://gre/modules/devtools/dbg-server.jsm');
return DebuggerServer.initialized;
});
assert.ok(debuggerServerInited, 'Debugger server initialized');
var debuggerServerSockets = client.executeScript(function() {
Cu.import('resource://gre/modules/devtools/dbg-server.jsm');
return DebuggerServer.listeningSockets;
});
assert.equal(debuggerServerSockets, 1, 'Debugger server listening');
});
});

View file

@ -0,0 +1,128 @@
/* global require, marionette, setup, suite, test */
'use strict';
var assert = require('chai').assert;
var appRoot = require('app-root-path');
// TODO Change the path once requireFromApp becomes its own module
var fromApp = require(appRoot + '/shared/test/integration/require_from_app');
var Messages = fromApp('sms').require('lib/messages');
var InboxView = fromApp('sms').require('lib/views/inbox/view');
var ThreadGenerator = fromApp('sms').require('generators/thread');
var Storage = fromApp('sms').require('lib/storage.js');
var Contacts = fromApp('contacts').require('lib/contacts');
var ContactsData = fromApp('contacts').require('lib/contacts_data');
marionette('Contacts', function() {
var messagesApp;
var client = marionette.client({
prefs: {
'focusmanager.testmode': true
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var thread, storage, contactsData, inbox;
var testContact = {
givenName: ['FirstName'],
familyName: ['LastName'],
name: ['FirstName LastName'],
tel: [{
value: '5551234567',
type: ['home']
}],
};
setup(function() {
var contactsApp = new Contacts(client);
contactsApp.launch();
contactsData = new ContactsData(client);
contactsData.createMozContact(testContact);
// Change once bug 1140344 lands. Closing the application is needed,
// in order to be sure to switch to the right frame when an activity is
// started.
contactsApp.close();
messagesApp = Messages.create(client);
storage = Storage.create(client);
client.contentScript.inject(
fromApp('sms').filePath('mocks/mock_test_storages.js')
);
client.contentScript.inject(
fromApp('sms').filePath('mocks/mock_navigator_moz_mobile_message.js')
);
messagesApp.launch();
inbox = new InboxView(client);
});
suite('Conversation Header', function() {
var conversation;
setup(function() {
thread = ThreadGenerator.generate();
storage.setMessagesStorage([thread], ThreadGenerator.uniqueMessageId);
conversation = inbox.goToConversation(thread.id);
});
// Deactivated because of bug 1175080. Sometimes Marionette looses the
// connection, which is making some intermittent Gij runs.
test.skip('should create a new contact', function() {
// Bug 1175080. Asserting on the throw makes the error message clearer.
// Otherwise we just have a crash in the stackwalk.
assert.doesNotThrow(function() {
var newContact = conversation.openCreateNewContact();
newContact.enterContactDetails({
givenName: 'Given',
familyName: 'Family',
});
messagesApp.switchTo();
assert.equal(conversation.carrierHeaderPhoneNumber, '+123');
assert.equal(conversation.headerTitle, 'Given Family');
});
});
// Deactivated because of bug 1175080. Sometimes Marionette looses the
// connection, which is making some intermittent Gij runs.
test.skip('should add the phone number to an existing contact', function() {
// Bug 1175080. Asserting on the throw makes the error message clearer.
// Otherwise we just have a crash in the stackwalk.
assert.doesNotThrow(function() {
var contactPicker = conversation.openAddToExistingContact();
contactPicker.tapContact(testContact.name);
contactPicker.tapUpdate();
messagesApp.switchTo();
assert.equal(conversation.carrierHeaderPhoneNumber, '+123');
assert.equal(conversation.headerTitle, testContact.name);
});
});
});
suite('add recipient', function() {
var newMessage;
setup(function() {
storage.setMessagesStorage();
newMessage = inbox.createNewMessage();
});
test('should pick contacts from Contacts', function() {
var contactPicker = newMessage.openContactPicker();
contactPicker.tapContact(testContact.name);
messagesApp.switchTo();
var recipients = newMessage.recipients;
assert.lengthOf(recipients, 1);
assert.deepEqual(recipients, testContact.name);
});
});
});

View file

@ -0,0 +1,59 @@
/* global require, marionette, setup, suite, test */
'use strict';
var assert = require('chai').assert;
var appRoot = require('app-root-path');
// TODO Change the path once requireFromApp becomes its own module
var fromApp = require(appRoot + '/shared/test/integration/require_from_app');
var Messages = fromApp('sms').require('lib/messages');
var InboxView = fromApp('sms').require('lib/views/inbox/view');
var ThreadGenerator = fromApp('sms').require('generators/thread');
var Storage = fromApp('sms').require('lib/storage.js');
marionette('Dialer', function() {
var MOCKS = [
fromApp('sms').filePath('mocks/mock_test_storages.js'),
fromApp('sms').filePath('mocks/mock_test_blobs.js'),
fromApp('sms').filePath('mocks/mock_navigator_moz_mobile_message.js')
];
var messagesApp;
var client = marionette.client({
prefs: {
'focusmanager.testmode': true
},
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var thread;
var storage;
setup(function() {
messagesApp = Messages.create(client);
storage = Storage.create(client);
MOCKS.forEach(function(mock) {
client.contentScript.inject(mock);
});
});
suite('Call button', function() {
setup(function() {
thread = ThreadGenerator.generate();
storage.setMessagesStorage([thread], ThreadGenerator.uniqueMessageId);
messagesApp.launch();
});
test('should go to dialer with the correct phone number', function() {
var inbox = new InboxView(client);
var conversation = inbox.goToConversation(thread.id);
var dialer = conversation.callContact();
assert.equal(dialer.phoneNumber, thread.participants[0]);
});
});
});

View file

@ -0,0 +1,19 @@
{
"extends": "../.jshintrc",
"curly": false,
"evil": true,
"node": true,
"validthis": true,
"globals": {
"Promise": true
},
"predef": [
"suite",
"suiteSetup",
"suiteTeardown",
"teardown",
"test"
]
}

View file

@ -0,0 +1,3 @@
{
"raisesAccessibilityExceptions": true
}

View file

@ -0,0 +1,135 @@
'use strict';
var Transform = require('stream').Transform;
var SEPARATOR = ':';
var SEPARATOR_CODE = SEPARATOR.charCodeAt(0);
/**
* First ocurrence of where string occurs in a buffer.
*
* NOTE: this is not UTF8 safe generally we expect to find the correct
* char fairly quickly unless the buffer is incorrectly formatted.
*
* @param {Buffer} buffer haystack.
* @param {String} code needle.
* @return {Numeric} -1 if not found index otherwise.
*/
function indexInBuffer(buffer, code) {
if (buffer.length === 0)
return -1;
var index = 0;
var length = buffer.length;
do {
if (buffer[index] === code) return index;
} while (
++index && index + 1 < length
);
return -1;
}
/**
* converts an object to a string representation suitable for storage on disk.
* Its very important to note that the length in the string refers to the utf8
* size of the json content in bytes (as utf8) not the JS string length.
*
* @param {Object} object to stringify.
* @return {String} serialized object.
*/
function stringify(object) {
var json = JSON.stringify(object);
var len = Buffer.byteLength(json);
return len + SEPARATOR + json;
}
/**
* attempts to parse a given buffer or string.
*
* @param {Uint8Array|Buffer} input in byteLength:{json..} format.
* @return {Objec} JS object.
*/
function parse(input) {
var stream = new Stream();
var result;
stream.once('data', function(data) {
result = data;
});
stream.write(input);
if (!result) {
throw new Error(
'no command available from parsing:' + input
);
}
return result;
}
function Stream() {
this._pendingLength = null;
this._buffer = new Buffer(0);
Transform.call(this, { objectMode: true });
}
Stream.prototype = {
__proto__: Transform.prototype,
_transform: function(chunk, encoding, cb) {
if (!this._pendingLength) {
var idx = indexInBuffer(chunk, SEPARATOR_CODE);
// Nothing to do just buffer it...
if (idx === -1) {
this._buffer = Buffer.concat([this._buffer, chunk]);
return cb();
}
// number of bytes in the json segment...
var length = Buffer.concat([this._buffer, chunk.slice(0, idx)]);
this._pendingLength = parseInt(length.toString(), 10);
this._buffer = new Buffer(0);
// We have transitioned to a pending length state so do another pass.
return this._transform(chunk.slice(idx + 1), encoding, cb);
}
// Total length too small nothing to do...
if (this._buffer.length + chunk.length < this._pendingLength) {
this._buffer = Buffer.concat([this._buffer, chunk]);
return cb();
}
var buffer = Buffer.concat([this._buffer, chunk]);
var remainder = null;
if (buffer.length > this._pendingLength) {
remainder = buffer.slice(this._pendingLength);
buffer = buffer.slice(0, this._pendingLength);
}
// Reset internal state and push current json string.
this._pendingLength = null;
this._buffer = new Buffer(0);
this.push(JSON.parse(buffer.toString()));
// If we have any remaining data we need to keep processing...
if (remainder) {
return this._transform(remainder, encoding, cb);
}
// Otherwise we are done yay....
return cb();
}
};
exports.parse = parse;
exports.stringify = stringify;
exports.Stream = Stream;
exports.separator = SEPARATOR;

View file

@ -0,0 +1,7 @@
{
"name": "json-wire-protocol",
"version": "1.0.0",
"author": "James Lal",
"main": "lib/index.js",
"description": "JSON wire protocol parser/writer (used in mozilla debugger protocol)"
}

View file

@ -0,0 +1,139 @@
'use strict';
var assert = require('assert');
var jsonWireProtocol = require('../');
suite('json wire protocol', function() {
var subject;
var TWO_BYTE = 'ž';
function createBytes(content) {
return new Buffer(content);
}
test('.separator', function() {
assert.ok(jsonWireProtocol.separator);
});
suite('#stringify', function() {
test('ASCII only', function() {
var input = { a: 'abcdefg' };
var expected = JSON.stringify(input);
expected = expected.length + ':' + expected;
var output = jsonWireProtocol.stringify(input);
assert.deepEqual(expected, output);
});
test('invalid strings', function() {
var invalid = 'xfoobar!';
assert.throws(function() {
jsonWireProtocol.parse(invalid);
});
});
test('with multibyte chars', function() {
var input = { a: TWO_BYTE + TWO_BYTE + TWO_BYTE };
var expected = JSON.stringify(input);
expected = '14:' + expected;
assert.deepEqual(jsonWireProtocol.stringify(input), expected);
});
});
suite('#parse', function() {
test('working string', function() {
var input = { woot: TWO_BYTE };
var string = jsonWireProtocol.stringify(input);
assert.deepEqual(
input,
jsonWireProtocol.parse(createBytes(string))
);
});
});
suite('.Stream', function() {
setup(function() {
subject = new jsonWireProtocol.Stream();
});
suite('#write', function() {
suite('multiple commands over two buffers', function() {
var commandA = { a: 'cool' };
var commandB = { b: 'woot' };
var commandC = { c: 'wtfman' };
var commandD = { d: TWO_BYTE };
var all = [
commandA,
commandB,
commandC
]
.map(jsonWireProtocol.stringify)
.join('');
var half = all.length / 2;
var bufferA = createBytes(all.slice(0, half));
var bufferB = createBytes(all.slice(half) +
jsonWireProtocol.stringify(commandD));
var parsed;
setup(function() {
parsed = [];
subject.on('data', function(result) {
parsed.push(result);
});
subject.write(bufferA);
subject.write(bufferB);
});
test('result after writing to stream', function() {
assert.deepEqual(
parsed,
[commandA, commandB, commandC, commandD]
);
});
});
test('multiple chunks until length', function(done) {
var expected = { longer: TWO_BYTE + 'a' + TWO_BYTE };
var string = jsonWireProtocol.stringify(expected);
subject.on('data', function(result) {
assert.deepEqual(result, expected);
done();
});
for (var i = 0; i < string.length; i++) {
subject.write(createBytes(string[i]));
}
});
suite('entire buffer', function() {
var buffer;
/* 13 ascii bytes */
var string = '{"one":"foo"}';
setup(function() {
buffer = createBytes('13:' + string);
});
test('read entire buffer', function(done) {
var expected = { one: 'foo' };
subject.once('data', function(data) {
assert.deepEqual(data, expected);
done();
});
subject.write(buffer);
});
});
});
});
});

View file

@ -0,0 +1,264 @@
/* global Marionette */
(function(module, ns) {
'use strict';
/**
* Actions is used to denote a set of actions that
* have to be executed in particular order.
*
* @class Marionette.Actions
* @param {Marionette.Client} context of a client.
*/
function Actions(client) {
this.client = client;
this.actionChain = [];
this.currentId = null;
}
Actions.prototype = {
/**
* Send a 'touchstart' event to this element. If no coordinates are given,
* it will be targeted at the center of the element.
* If given, it will be targeted at the (x,y) coordinates
* relative to the top-left corner of the element.
*
* @method press
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* to press on.
* @param {Number} x optional, x-coordinate to tap,
* relative to the top-left corner of the element.
* @param {Number} y optional, y-coordinate to tap,
* relative to the top-left corner of the element.
* @return {Object} self.
*/
press: function press(element, x, y) {
this.actionChain.push(['press', element.id, x, y]);
return this;
},
/**
* release() can only be called
* if press() has already be called on this element.
* Send 'touchend' event to whenever the finger is.
*
* @method release
*
* @return {Object} self.
*/
release: function release() {
this.actionChain.push(['release']);
return this;
},
/**
* move() can only be called
* if press() has already be called on this element.
* move() send 'touchmove' event which moves the finger to target element.
*
* @method move
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* of the move gesture.
*
* @return {Object} self.
*/
move: function move(element) {
this.actionChain.push(['move', element.id]);
return this;
},
/**
* moveByOffset() can only be called
* if press() has already be called on this element.
* moveByOffset() send 'touchmove' event.
*
* @method moveByOffset
*
* @param {Number} x x-coordinate relative to the top-left corner of
* the target element of the last touch.
*
* @param {Number} y y-coordinate relative to the top-left corner of
* the target element of the last touch.
*
* @return {Object} self.
*/
moveByOffset: function moveByOffset(x, y) {
this.actionChain.push(['moveByOffset', x, y]);
return this;
},
/**
* wait() waits for specified time period.
*
* @method wait
* @param {Number} time wait for "time" seconds.
*
* @return {Object} self.
*/
wait: function wait(time) {
this.actionChain.push(['wait', time]);
return this;
},
/**
* cancel() can only be called
* if press() has already be called on this element.
* cancel() send 'touchcancel' event to whenever the finger is.
*
* @method cancel
*
* @return {Object} self.
*/
cancel: function cancel() {
this.actionChain.push(['cancel']);
return this;
},
/**
* tap() performs a quick tap on the target.
* action.tap() is essentially action.press().release().
*
* @method tap
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* to press/release on.
*
* @param {Number} x optional, x-coordinate relative to the top-left corner
* of the element of the last touch.
*
* @param {Number} y optional, y-coordinate relative to the top-left corner
* of the element of the last touch.
*
* @return {Object} self.
*/
tap: function tap(element, x, y) {
this.actionChain.push(['press', element.id, x, y]);
this.actionChain.push(['release']);
return this;
},
/**
* doubleTap() performs a double
* {{#crossLink "Marionette.Actions/tap"}}{{/crossLink}} on the element.
*
* @method doubleTap
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* to double tap on.
*
* @param {Number} x optional, x-coordinate relative to
* the top-left corner of the element of the last touch.
*
* @param {Number} y optional, y-coordinate relative to
* the top-left corner of the element of the last touch.
*
* @return {Object} self.
*/
doubleTap: function doubleTap(element, x, y) {
this.actionChain.push(['press', element.id, x, y]);
this.actionChain.push(['release']);
this.actionChain.push(['press', element.id, x, y]);
this.actionChain.push(['release']);
return this;
},
/**
* flick() sends a sequence of touch events:
* touchstart, touchmove, touchend.
* It scrolls the page in any direction
* within period of time provided(duration).
*
* @method flick
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* to double tap on.
*
* @param {Number} x1 starting x-coordinates of
* the finger relative to the element.
*
* @param {Number} y1 starting y-coordinates of
* the finger relative to the element.
*
* @param {Number} x2 ending x-coordinates of
* the finger relative to the element.
*
* @param {Number} y2 ending y-coordinates of
* the finger relative to the element.
*
* @param {Number} duration optional,
* time needed for the flick gesture for complete.
*
* @return {Object} self.
*/
flick: function flick(element, x1, y1, x2, y2, duration) {
var time = 0;
var timeIncrement = 10.0;
if (duration === undefined) {
duration = 200;
}
if (timeIncrement >= duration) {
timeIncrement = duration;
}
var moveX = timeIncrement / duration * (x2 - x1);
var moveY = timeIncrement / duration * (y2 - y1);
this.actionChain.push(['press', element.id, x1, y1]);
while (time < duration) {
time += timeIncrement;
this.actionChain.push(['moveByOffset', moveX, moveY]);
this.actionChain.push(['wait', timeIncrement / 1000]);
}
this.actionChain.push(['release']);
return this;
},
/**
* Sends 'touchstart',
* then wait for 'time' seconds, and send 'touchend' eventually.
* longPress() cannot follow any active touch,
* i.e. the finger must leave the screen before longPress() gets called.
*
* @method longPress
* @param {Object} element {{#crossLink "Marionette.Element"}}{{/crossLink}}
* of the long press.
*
* @param {Number} time the waiting time between touchstart and touchend.
*
* @return {Object} self.
*/
longPress: function longPress(element, time) {
this.actionChain.push(['press', element.id]);
this.actionChain.push(['wait', time]);
this.actionChain.push(['release']);
return this;
},
/**
* perform() will send the whole action chain built
* so far to the server side for execution.
*
* @method perform
* @param {Function} callback callback when the perform completes.
*
* @return {Object} self.
*/
perform: function perform(callback) {
var cmd = {
name: 'actionChain',
parameters: {
chain: this.actionChain,
nextId: this.currentId
}
};
this.currentId = this.client._sendCommand(cmd, callback, 'value');
this.actionChain = [];
return this;
}
};
module.exports = Actions;
}.apply(
this,
(this.Marionette) ?
[Marionette('actions'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,94 @@
/* global Marionette */
(function(module, ns) {
'use strict';
var debug = require('debug')('marionette:command-stream');
var Responder = ns.require('responder');
var wire = require('json-wire-protocol');
/**
* Command stream accepts a socket or any event
* emitter that will emit data events
*
* @class Marionette.CommandStream
* @param {EventEmitter} socket socket instance.
* @constructor
*/
function CommandStream(socket) {
this.socket = socket;
this._handler = new wire.Stream();
this._handler.on('data', this.emit.bind(this, this.commandEvent));
Responder.apply(this);
socket.on('data', this._handler.write.bind(this._handler));
socket.on('error', function(err) {
this.emit('error', err);
}.bind(this));
}
var proto = CommandStream.prototype = Object.create(
Responder.prototype
);
/**
* name of the event this class
* will emit when a response to a
* command is received.
*
* @property commandEvent
* @type String
*/
proto.commandEvent = 'command';
/**
* Parses command into a string to
* be sent over a tcp socket to marionette.
*
*
* @method stringify
* @param {Object} command marionette command.
* @return {String} command as a string.
*/
proto.stringify = function stringify(command) {
return wire.stringify(command);
};
/**
* Writes a command to the socket.
* Handles conversion and formatting of object.
*
* @method send
* @param {Object} data marionette command.
*/
proto.send = function send(data) {
debug('writing ', data, 'to socket');
if (this.socket.write) {
//nodejs socket
this.socket.write(this.stringify(data), 'utf8');
} else {
//moztcp socket
this.socket.send(this.stringify(data));
}
};
/**
* Adds a chunk (string or buffer) to the
* total buffer of this instance.
*
* @this
* @param {String|Buffer} buffer buffer or string to add.
*/
proto.add = function add(buffer) {
this._handler.write(buffer);
};
module.exports = exports = CommandStream;
}.apply(
this,
(this.Marionette) ?
[Marionette('command-stream'), Marionette] :
[module, require('./marionette')]
));

View file

@ -0,0 +1,231 @@
/* global Marionette */
(function(module, ns) {
'use strict';
var Command = require('../message').Command;
var Response = require('../message').Response;
/**
*
* Abstract driver that will handle
* all common tasks between implementations.
* Such as error handling, request/response queuing
* and timeouts.
*
* @constructor
* @class Marionette.Drivers.Abstract
* @param {Object} options set options on prototype.
*/
function Abstract(options) {
this._sendQueue = [];
this._responseQueue = [];
this._lastId = 0;
}
Abstract.prototype = {
/**
* Duration in milliseconds to wait before timing out a command
* request.
*
* @property timeout
* @type Number
*/
timeout: 10000,
/**
* Waiting for a command to finish?
*
* @private
* @property _waiting
* @type Boolean
*/
_waiting: true,
/**
* Is system ready for commands?
*
* @property ready
* @type Boolean
*/
ready: false,
/**
* A unique identifier for the current connection.
*
* @property connectionId
* @type Number
*/
connectionId: null,
/**
* Duration in milliseconds defining when to abort an attempt to
* execute script in the remote end.
*
* @method setScriptTiemout
* @param {Number} the timeout value.
*/
setScriptTimeout: function setScriptTimeout(timeout) {
},
/**
* Sends a JSON data structure across the wire to the remote end.
*
* Each command is queued whilst waiting for any pending commands.
* This ensures order of response is correct, but also means the client
* implements a synchronous interface for Marionette.
*
* @method send
*
* @param {(Object|Command|Response)} obj
* An object that can be dumped into a JSON data structure,
* or a message that can be marshaled.
* @param {Function} callback
* Callback to be invoked when a response for the request is
* received.
*/
send: function send(obj, callback) {
if (!this.ready) {
throw new Error('Connection is not ready');
}
if (typeof callback == 'undefined') {
throw new Error('Callback is required');
}
if (obj instanceof Command || obj instanceof Response) {
this.sendMessage(obj, callback);
} else {
this.sendRaw(obj, callback);
}
return this;
},
/**
* Sends a message across the wire to the remote end.
*
* @param {(Command|Response)} message
* The message to send.
* @param {Function} callback
* Callback to be invoked when a response for the request is
* received.
*/
sendMessage: function(message, callback) {
message.id = ++this._lastId;
this.sendRaw(message.toMsg(), callback);
},
/**
* Sends a JSON data structure across the wire to the remote end.
*
* @param {Object} packet
* An object that can be marshaled into a JSON data structure.
* @param {Function} callback
* Callback to be invoked when a response for the request is
* received.
*/
sendRaw: function(packet, callback) {
this._responseQueue.push(callback);
this._sendQueue.push(packet);
this._nextCommand();
},
/**
* Connects to the remote end.
*
* Requires the _connect function to be defined in the concrete class.
*
* MyConcreteClass.prototype._connect = function _connect(){
* // open a socket to marrionete accept response
* // you *must* call _onDeviceResponse with the first
* // response from Marionette it looks like this:
* // {applicationType: "gecko", marionetteProtocol: 2}
* this.connectionId = result.id;
* }
*
* @method connect
* @param {Function} callback executes
* after successfully connecting to the server.
*/
connect: function connect(callback) {
this.ready = true;
this._responseQueue.push(function(data) {
this.marionetteProtocol = data.marionetteProtocol || 1;
this.traits = data.traits;
this.applicationType = data.applicationType;
callback();
}.bind(this));
this._connect();
},
/**
* Destroys connection to server
*
* Will immediately close connection to server
* closing any pending responses.
*
* @method close
*/
close: function() {
this.ready = false;
this._responseQueue.length = 0;
if (this._close) {
this._close();
}
},
/**
* Checks queue if not waiting for a response
* Sends command to websocket server
*
* @private
* @method _nextCommand
*/
_nextCommand: function _nextCommand() {
if (!this._waiting && this._sendQueue.length) {
this._waiting = true;
var next = this._sendQueue.shift();
this._sendCommand(next);
}
},
/**
* Handles responses from devices.
* Will only respond to the event if the connectionId
* is equal to the event id and the client is ready.
*
* @param {Object} data response from server.
* @private
* @method _onDeviceResponse
*/
_onDeviceResponse: function _onDeviceResponse(data) {
if (this.ready && data.id === this.connectionId) {
this._waiting = false;
var resp;
if (this.marionetteProtocol >= 3) {
var msg = Response.fromMsg(data.response);
resp = msg.result;
} else {
resp = data.response;
}
var cb = this._responseQueue.shift();
cb(resp);
this._nextCommand();
}
}
};
module.exports = Abstract;
}.apply(
this,
(this.Marionette) ?
[Marionette('drivers/abstract'), Marionette] :
[module, require('../marionette')]
));

View file

@ -0,0 +1,23 @@
/* global Marionette */
(function(module, ns) {
'use strict';
module.exports = {
Abstract: ns.require('drivers/abstract')
};
if (typeof(window) === 'undefined') {
module.exports.Tcp = require('./tcp');
module.exports.TcpSync = require('./tcp-sync');
} else {
if (typeof(window.TCPSocket) !== 'undefined') {
module.exports.MozTcp = ns.require('drivers/moz-tcp');
}
}
}.apply(
this,
(this.Marionette) ?
[Marionette('drivers'), Marionette] :
[module, require('../marionette')]
));

View file

@ -0,0 +1,233 @@
'use strict';
var debug = require('debug')('marionette:tcp-sync');
var wire = require('json-wire-protocol');
var sockittome = require('sockit-to-me');
var Command = require('../message').Command;
var Response = require('../message').Response;
var DEFAULT_HOST = 'localhost';
var DEFAULT_PORT = 2828;
var SOCKET_TIMEOUT_EXTRA = 500;
function TcpSync(options) {
if (!options) {
options = {};
}
this.host = options.host || DEFAULT_HOST;
this.port = options.port || DEFAULT_PORT;
this.connectionTimeout = options.connectionTimeout || 2000;
this.isSync = true;
this.retryInterval = 300;
this.sockit = new sockittome.Sockit();
this.sockit.setPollTimeout(this.connectionTimeout + SOCKET_TIMEOUT_EXTRA);
this._lastId = 0;
}
TcpSync.prototype._createSocketConfig = function() {
return {host: this.host, port: this.port};
};
TcpSync.prototype.setScriptTimeout = function(timeout) {
this.sockit.setPollTimeout(timeout + SOCKET_TIMEOUT_EXTRA);
};
/**
* Utility to wait for the marionette socket to be ready.
*
* @method waitForSocket
* @param {Object} [options] for timeout.
* @param {Number} [options.interval] time between running test.
* @param {Number} [options.timeout] maximum wallclock time before
* failing test.
* @param {Function} [callback] callback.
*/
TcpSync.prototype.waitForSocket = function(options, callback) {
// the logic is lifted from
// mozilla-central/source/testing/marionette/client/marionette/marionette.py
if (typeof(options) === 'function') {
callback = options;
options = null;
}
options = options || {};
var interval = options.interval || 100;
var timeout = options.timeout || 300000;
var socketTimeout = 1000;
var sockit = new sockittome.Sockit();
var start = Date.now();
var lastDebugMessage = '';
var self = this;
// Use sockittome's built in polling timeout during calls to connect
// to avoid socket misuse.
sockit.setPollTimeout(socketTimeout);
var probeSocket = function() {
try {
sockit.connect(this._createSocketConfig());
var s = sockit.read(16).toString();
sockit.close();
if (s.indexOf(':') != -1) {
callback();
return;
}
} catch (e) {
// This may seem ridiculous, but we have to keep these errors showing up
// but, they often repeat like CRAZY, so, quiet them down by only showing
// each exception we encounter once.
if (lastDebugMessage != e.message) {
lastDebugMessage = e.message;
debug('exception when probing socket', lastDebugMessage);
}
// Above read _may_ fail so it is important to close the socket...
sockit.close();
}
// timeout. Abort.
if ((Date.now() - start) > timeout) {
console.error('Timeout connecting to B2G');
return;
}
// interval delay for the next iteration.
setTimeout(probeSocket.bind(self), interval);
}.bind(this);
debug('probing socket');
probeSocket();
};
TcpSync.prototype.connect = function(callback) {
this.waitForSocket(function _connect() {
try {
this.sockit.connect(this._createSocketConfig());
} catch(err) {
if (Date.now() - this._beginConnect >= this.connectionTimeout) {
callback(err);
}
setTimeout(_connect.bind(this, callback), this.retryInterval);
return;
}
if (!this._beginConnect) {
this._beginConnect = Date.now();
}
// Ensure this method's resolution is asynchronous in all cases
process.nextTick(function() {
debug('socket connected');
delete this._beginConnect;
var resp = this._readResponse();
this.marionetteProtocol = resp.marionetteProtocol || 1;
this.traits = resp.traits;
this.applicationType = resp.applicationType;
callback();
}.bind(this));
}.bind(this));
};
TcpSync.prototype.defaultCallback = function(err, result) {
if (err) {
throw err;
}
return result;
};
// Following the json-wire-protocol implementation, read one byte at a time
// until the 'separator' character is received. The data preceding the
// delimiter describes the length of the JSON string.
TcpSync.prototype._readResponse = function() {
var stream = new wire.Stream();
var data, error;
stream.on('data', function(parsed) {
data = parsed;
});
stream.on('error', function(err) {
error = err;
});
while (!data && !error) {
stream.write(this.sockit.read(1));
}
if (error) {
throw error;
}
if (this.marionetteProtocol >= 3) {
var resp = Response.fromMsg(data);
return resp.error || resp.result;
}
return data;
};
/**
* Sends a JSON data structure across the wire to the remote end.
*
* @param {(Object|Command|Response)} obj
* An object that can be dumped into a JSON data structure,
* or a message that can be marshaled.
* @param {Function} cb
* Callback to be invoked when a response for the request is
* received.
*
* @return {?}
* The return value from the passed in callback.
*/
TcpSync.prototype.send = function(obj, cb) {
if (obj instanceof Command || obj instanceof Response) {
return this.sendMessage(obj, cb);
} else {
return this.sendRaw(obj, cb);
}
};
/**
* Sends a message across the wire to the remote end.
*
* @param {(Command|Response)} msg
* The message to send.
* @param {Function} cb
* Callback to be invoked when a response for the request is received.
*
* @return {?}
* The return value from the passed in callback.
*/
TcpSync.prototype.sendMessage = function(msg, cb) {
msg.id = ++this._lastId;
return this.sendRaw(msg.toMsg(), cb);
};
/**
* Sends a JSON data structure across the wire to the remote end.
*
* @param {Object} data
* An object that can be marshaled into a JSON data structure.
* @param {Function} cb
* Callback to be invoked when a response for the request is received.
*
* @return {?}
* The return value from the passed in callback.
*/
TcpSync.prototype.sendRaw = function(data, cb) {
debug('write', data);
this.sockit.write(wire.stringify(data));
return cb(this._readResponse());
};
TcpSync.prototype.close = function() {
this.sockit.close();
};
module.exports = TcpSync;

View file

@ -0,0 +1,106 @@
'use strict';
var Abstract = require('./abstract');
var CommandStream = require('../../marionette/command-stream');
var debug = require('debug')('marionette:tcp');
var net = require('net');
var retrySocket = require('socket-retry-connect');
/** TCP **/
Tcp.Socket = net.Socket;
/**
* NodeJS only tcp socket driver for marionette.
* See {{#crossLink "Marionette.Drivers.MozTcp"}}{{/crossLink}}
* for the gecko/xpcom vesion of this driver.
*
* @class Marionette.Drivers.Tcp
* @extends Marionette.Drivers.Abstract
* @constructor
* @param {Options} options connection options.
* @param {String} [options.host="localhost"] host.
* @param {Numeric} [options.port="2828"] port.
*/
function Tcp(options) {
if (!options) {
options = {};
}
Abstract.call(this, options);
/**
* The unique identifier of the current connection.
*
* @property connectionId
* @type Number
*/
this.connectionId = 0;
/**
* The remote hostname.
*
* @property host
* @type String
*/
this.host = options.host || 'localhost';
/**
* The remote port number.
*
* @property port
* @type Number
*/
this.port = options.port || 2828;
}
Tcp.prototype = Object.create(Abstract.prototype);
/**
* Sends a command to the server.
*
* @param {Object} cmd remote marionette command.
*/
Tcp.prototype._sendCommand = function _sendCommand(cmd) {
this.client.send(cmd);
};
/**
* Opens TCP socket for marionette client.
*/
Tcp.prototype._connect = function connect() {
var options = {
port: this.port,
host: this.host,
tries: this.tries
};
retrySocket.waitForSocket(options, function(err, socket) {
debug('got socket starting command stream');
this.socket = socket;
this.client = new CommandStream(this.socket);
this.client.on('command', this._onClientCommand.bind(this));
}.bind(this));
};
/**
* Receives command from server.
*
* @param {Object} data response from marionette server.
*/
Tcp.prototype._onClientCommand = function(data) {
this._onDeviceResponse({
id: this.connectionId,
response: data
});
};
/**
* Closes connection to marionette.
*/
Tcp.prototype._close = function close() {
if (this.socket && this.socket.destroy) {
this.socket.destroy();
}
};
/** export */
module.exports = exports = Tcp;

View file

@ -0,0 +1,361 @@
/* global Marionette */
/**
@namespace
*/
(function(module, ns) {
'use strict';
/**
* Creates an element reference
* based on an id and a client instance.
* You should never need to manually create
* an instance of element.
*
* Use {{#crossLink "Marionette.Client/findElement"}}{{/crossLink}} or
* {{#crossLink "Marionette.Client/findElements"}}{{/crossLink}} to create
* instance(s) of this class.
*
* @class Marionette.Element
* @param {String} id id of element.
* @param {Marionette.Client} client client instance.
*/
function Element(id, client) {
this.id = id;
this.client = client;
}
Element.prototype = {
/**
* Sends remote command processes the result.
* Appends element id to each command.
*
* @method _sendCommand
* @chainable
* @private
* @param {Object} command marionette request.
* @param {Function} callback callback function receives the result of
* response[key] as its first argument.
* @param {String} key key in the response to pass to callback.
*
* @return {Object} self.
*/
_sendCommand: function(command, callback, key) {
if (!command.parameters) {
command.parameters = {};
}
if (typeof command.parameters.id == 'undefined') {
command.parameters.id = this.id;
}
var isSync = this.client.isSync;
var result = this.client._sendCommand(command, callback, key);
if (isSync)
return result;
return this;
},
/**
* Finds a single child of this element.
*
* @method findElement
* @param {String} query search string.
* @param {String} method search method.
* @param {Function} callback element callback.
* @return {Object} self.
*/
findElement: function findElement(query, method, callback) {
var result = this.client.findElement(query, method, this.id, callback);
if (this.client.isSync) {
return result;
}
return this;
},
/**
* Finds a all children of this element that match a pattern.
*
* @method findElements
* @param {String} query search string.
* @param {String} method search method.
* @param {Function} callback element callback.
* @return {Array} elements matched the query string.
*/
findElements: function findElement(query, method, callback) {
var result = this.client.findElements(query, method, this.id, callback);
if (this.client.isSync) {
return result;
}
return this;
},
/**
* Shortcut method to execute
* a function with this element as first argument.
* @method scriptWith
* @param {Function|String} script remote script.
* @param {Array} [args] optional arguments for script.
* @param {Function} callback callback when script completes.
* @return {String|Number} return value of the script.
*/
scriptWith: function scriptWith(script, args, callback) {
if (!Array.isArray(args)) {
callback = args;
args = [];
}
return this.client.executeScript(script, [this].concat(args), callback);
},
/**
* Checks to see if two elements are equal
*
* @method equals
* @param {Marionette.Element} element element to test.
* @return {Boolean} true when equal.
*/
equals: function equals(element) {
if (!(element instanceof Element)) {
throw new Error('must pass an element');
}
return this.id === element.id;
},
/**
* Gets attribute value for element.
*
* @method getAttribute
* @param {String} attr attribtue name.
* @param {Function} callback gets called with attribute's value.
* @return {String} the value of the Attribute.
*/
getAttribute: function getAttribute(attr, callback) {
var body = {
name: 'getElementAttribute',
parameters: {
name: attr,
},
};
return this._sendCommand(body, callback, 'value');
},
/**
* Sends typing event keys to element.
*
*
* @method sendKeys
* @param {String|Array} input message to type.
* @param {Function} callback boolean success.
* @return {Object} self.
*/
sendKeys: function sendKeys(input, callback) {
if (!Array.isArray(input)) {
input = [input];
}
var body = {
name: 'sendKeysToElement',
parameters: {
value: input,
},
};
return this._sendCommand(body, callback);
},
/**
* Clicks element.
*
* @method click
* @param {Function} callback boolean result.
* @return {Object} self.
*/
click: function click(callback) {
var body = {name: 'clickElement'};
return this._sendCommand(body, callback);
},
/**
* Gets text of element
*
* @method text
* @param {Function} callback text of element.
* @return {String} text of element.
*/
text: function text(callback) {
var body = {name: 'getElementText'};
return this._sendCommand(body, callback, 'value');
},
/**
* Returns tag name of element.
*
* @method tagName
* @param {Function} callback node style [err, tagName].
* @return {String} tag name of element.
*/
tagName: function tagName(callback) {
var body = {name: 'getElementTagName'};
return this._sendCommand(body, callback, 'value');
},
/**
* Taps an element at given x and y coordinates.
* If no offsets are given, it will be tapped at the center of the element.
*
* @method tap
* @param {Number} [x] offset for the tap.
* @param {Number} [y] offset for the tap.
* @param {Function} [callback] [Error err]
* @return {object} self
*/
tap: function(x, y, callback) {
var body = {
name: 'singleTap',
parameters: {},
};
if (typeof x == 'number') {
body.parameters.x = x;
}
if (typeof y == 'number') {
body.parameters.y = y;
}
return this._sendCommand(body, callback, 'value');
},
/**
* Clears element.
*
* @method clear
* @param {Function} callback value of element.
* @return {Object} self.
*/
clear: function clear(callback) {
var body = {name: 'clearElement'};
return this._sendCommand(body, callback);
},
/**
* Checks if element is selected.
*
* @method selected
* @param {Function} callback boolean argument.
* @return {Object} self.
*/
selected: function selected(callback) {
var body = {name: 'isElementSelected'};
return this._sendCommand(body, callback, 'value');
},
/**
* Checks if element is enabled.
*
* @method enabled
* @param {Function} callback boolean argument.
* @return {Object} self.
*/
enabled: function enabled(callback) {
var body = {name: 'isElementEnabled'};
return this._sendCommand(body, callback, 'value');
},
/**
* Checks if element is displayed.
*
* @method displayed
* @param {Function} callback boolean argument.
* @return {Object} self.
*/
displayed: function displayed(callback) {
var body = {name: 'isElementDisplayed'};
return this._sendCommand(body, callback, 'value');
},
/**
* Returns the size of a given element.
*
* The returned size will be in the following format:
*
* // returned in callback or result of this call in the sync driver.
* {
* width: Number,
* height: Number
* }
*
* @method size
* @param {Function} callback [Error err, Object size].
* @return {Object} self.
*/
size: function size(callback) {
var body = {name: 'getElementRect'};
return this._sendCommand(
body, callback, this.client.protocol == 1 ? 'value' : undefined);
},
/**
* Returns the dictionary with x and y location of an element.
*
* The returned location will be in the following format:
*
* // returned in callback or result of this call in the sync driver.
* {
* x: Number,
* y: Number
* }
*
* @method location
* @param {Function} callback [Error err, Object location].
* @return {Object} self.
*/
location: function location(callback) {
var body = {name: 'getElementRect'};
return this._sendCommand(
body, callback, this.client.protocol == 1 ? 'value' : undefined);
},
/**
* Returns the object with:
* x and y location of the element
* height and width of the element
*
* @method rect
* @param {Function} callback [Error err, Object rect]
* @return {Object} self.
*/
rect: function rect (callback) {
var body = {name: 'getElementRect'};
return this._sendCommand(
body, callback, this.client.protocol == 1 ? 'value' : undefined);
},
/**
* Returns the value of the specified CSS property name.
*
* @method cssProperty
* @param {String} property css property.
* @param {Function} callback [Error err, String value].
* @return {Object} self.
*/
cssProperty: function cssProperty(property, callback) {
var body = {
name: 'getElementValueOfCssProperty',
parameters: {
propertyName: property,
},
};
return this._sendCommand(body, callback, 'value');
}
};
module.exports = Element;
}.apply(
this,
(this.Marionette) ?
[Marionette('element'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,131 @@
/* jshint -W069 */
/* global Marionette */
(function(module, ns) {
'use strict';
var ERRORS = Object.freeze({
'element not accessible': 'ElementNotAccessibleError',
'element not selectable': 'ElementIsNotSelectable',
'element not visible': 'ElementNotVisible',
'invalid cookie domain': 'InvalidCookieDomain',
'invalid element coordinates': 'InvalidElementCoordinates',
'invalid element state': 'InvalidElementState',
'invalid selector': 'InvalidSelector',
'invalid xpath selector': 'XPathLookupError',
'javascript error': 'JavaScriptError',
'no such alert': 'NoAlertOpenError',
'no such element': 'NoSuchElement',
'no such frame': 'NoSuchFrame',
'no such window': 'NoSuchWindow',
'script timeout': 'ScriptTimeout',
'stale element reference': 'StaleElementReference',
'timeout': 'Timeout',
'unable to set cookie': 'UnableToSetCookie',
'unexpected alert open': 'UnexpectedAlertOpen',
'unknown command': 'UnknownCommand',
'unknown error': 'UnknownError',
'webdriver error': 'GenericError',
});
var CODES = Object.freeze({
7: ERRORS['no such element'],
8: ERRORS['no such frame'],
9: ERRORS['unknown command'],
10: ERRORS['stale element reference'],
11: ERRORS['element not visible'],
12: ERRORS['invalid element state'],
13: ERRORS['unknown error'],
15: ERRORS['element not selectable'],
17: ERRORS['javascript error'],
19: ERRORS['invalid xpath selector'],
21: ERRORS['timeout'],
23: ERRORS['no such window'],
24: ERRORS['invalid cookie domain'],
25: ERRORS['unable to set cookie'],
26: ERRORS['unexpected alert open'],
27: ERRORS['no such alert'],
28: ERRORS['script timeout'],
29: ERRORS['invalid element coordinates'],
32: ERRORS['invalid selector'],
56: ERRORS['element not accessible'],
500: ERRORS['webdriver error']
});
var DEFAULT_ERROR = ERRORS['webdriver error'];
/**
* Returns an error object given an error object from the Marionette client.
*
* Codes are from:
* http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes
*
* Status strings are from:
* https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors
*
* The expected input for protocol version 2 and higher:
*
* {
* error: "javascript error",
* message: "Something",
* stacktrace: "wentwrong@line",
* }
*
* The expected input for protocol version 1:
*
* {
* message: "Something",
* stacktrace: "wentwrong@line",
* status: "javascript error",
* }
*
* @param {Client} client which the error originates from.
* @param {Object} options for error (see above).
*/
function MarionetteError(client, options) {
var error = DEFAULT_ERROR;
if (options.status in CODES) {
error = CODES[options.status];
} else if (options.status in ERRORS) {
error = ERRORS[options.status];
} else if (options.error in ERRORS) {
error = ERRORS[options.error];
}
this.client = client;
this.type = error;
this.name = this.type;
this.message = this.name;
if (options.message)
this.message += ': ' + options.message;
this.message += '\nRemote Stack:\n';
this.message += options.stacktrace || '<none>';
if (Error.captureStackTrace) {
Error.captureStackTrace(this);
} else {
// elsewhere we do horrible try/catch
try {
throw new Error();
} catch (e) {
this.stack = e.stack;
}
}
}
MarionetteError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error
}
});
MarionetteError.ERRORS = ERRORS;
MarionetteError.CODES = CODES;
module.exports = MarionetteError;
}.apply(
this,
(this.Marionette) ?
[Marionette('error'), Marionette] :
[module, require('./marionette')]
));

View file

@ -0,0 +1,128 @@
/* global Marionette */
(function(module, ns) {
'use strict';
function deepmerge(d) {
for (var i = 1; i < arguments.length; ++i) {
var o = arguments[i];
for (var k in o) {
var v = o[k];
if (typeof v == 'object') {
d[k] = deepmerge(d[k], v);
} else {
if (Array.isArray(d)) {
if (d.indexOf(v) < 0) {
d.push(v);
}
} else {
d[k] = v;
}
}
}
}
return d;
}
function cmd(defaults) {
return function(override) {
if (typeof(override) === 'undefined') {
override = {};
}
return deepmerge(defaults, override);
};
}
module.exports = {
connectProto1: cmd({from: 'root', applicationType: 'gecko', traits: []}),
connectProto2: cmd({applicationType: 'gecko', marionetteProtocol: 2}),
getMarionetteID: cmd({type: 'getMarionetteID'}),
getMarionetteIDResponse: cmd({from: 'root', id: 'con1'}),
newSession: cmd({type: 'newSession'}),
newSessionResponseProto1: cmd({
from: 'actor',
value: 'b2g-7',
}),
newSessionResponseProto2: cmd({
sessionId: '{2dddca75-7f78-415f-a7de-63eb2bc9412b}',
capabilities: {browserName: 'firefox'},
}),
getWindow: cmd({name: 'getWindow'}),
getWindowResponse: cmd({value: '3-b2g'}),
getWindows: cmd({name: 'getWindows'}),
getWindowsResponseProto1: cmd({from: 'actor', value: ['1-b2g', '2-b2g']}),
getWindowsResponseProto2: cmd(['1-b2g', '2-b2g']),
getUrl: cmd({name: 'getUrl'}),
getUrlResponse: cmd({value: 'http://localhost/'}),
getLogsResponseProto1: cmd({
from: 'actor',
value: [
['debug', 'wow', 'Fri Apr 27 2012 11:00:32 GMT-0700 (PDT)'],
],
}),
getLogsResponseProto2: cmd([
['debug', 'wow', 'Fri Apr 27 2012 11:00:32 GMT-0700 (PDT)'],
]),
screenshotResponse: cmd({
value: 'data:image/png;base64,iVBOgoAAAANSUhEUgAAAUAAAAHMCAYAAACk4nEJA',
}),
elementEqualsResponse: cmd({value: false}),
findElementResponse: cmd(
{value: {ELEMENT: '{8056e6f7-2213-41d4-9db6-ad77ac7a96d3}'}}),
findElementsResponseProto1: cmd({
from: 'actor',
value: [
{ELEMENT: '{46982c9e-bb0c-486e-a514-d1cf20b42641}'},
{ELEMENT: '{585e70ee-e088-43cc-9b07-a4b078c1b8db}'},
],
}),
findElementsResponseProto2: cmd([
{ELEMENT: '{46982c9e-bb0c-486e-a514-d1cf20b42641}'},
{ELEMENT: '{585e70ee-e088-43cc-9b07-a4b078c1b8db}'},
]),
numberError: cmd({
from: 'actor',
error: {
message: 'you fail',
status: 7,
stacktrace: 'fail@url\nother:300',
},
}),
stringError: cmd({
from: 'actor',
error: {
message: 'you fail',
status: 'no such element',
stacktrace: 'fail@url\nother:300',
},
}),
modernError: cmd({
error: 'no such element',
message: 'you fail',
stacktrace: 'fail@url\nother:300',
}),
//valueProto1: cmd({from: 'actor', value: 'zomg'}),
value: cmd({value: 'zomg'}),
capabilities: cmd({capabilities: {browserName: 'firefox'}}),
//okProto1: cmd({from: 'actor', ok: true}),
ok: cmd({}),
};
}.apply(
this,
(this.Marionette) ?
[Marionette('example-commands'), Marionette] :
[module, require('./marionette')]
));

View file

@ -0,0 +1,20 @@
/* global Marionette */
(function(module, ns) {
'use strict';
var exports = module.exports;
exports.Element = ns.require('element');
exports.Error = ns.require('error');
exports.Client = ns.require('client');
exports.Drivers = ns.require('drivers');
exports.CommandStream = ns.require('command-stream');
exports.Actions = ns.require('actions');
exports.MultiActions = ns.require('multi-actions');
}.apply(
this,
(this.Marionette) ?
[Marionette, Marionette] :
[module, require('./marionette')]
));

View file

@ -0,0 +1,101 @@
(function(global, module) {
'use strict';
/**
* Define a list of paths
* this will only be used in the browser.
*/
var paths = {};
/**
* Exports object is a shim
* we use in the browser to
* create an object that will behave much
* like module.exports
*/
function Exports(path) {
this.path = path;
}
Exports.prototype = {
/**
* Unified require between browser/node.
* Path is relative to this file so you
* will want to use it like this from any depth.
*
*
* var Leaf = ns.require('sub/leaf');
*
*
* @param {String} path path lookup relative to this file.
*/
require: function exportRequire(path) {
if (typeof(window) === 'undefined') {
return require(require('path').join(__dirname, path));
} else {
return paths[path];
}
},
/**
* Maps exports to a file path.
*/
set exports(val) {
return (paths[this.path] = val);
},
get exports() {
return paths[this.path];
}
};
/**
* Module object constructor.
*
*
* var module = Module('sub/leaf');
* module.exports = function Leaf(){}
*
*
* @constructor
* @param {String} path file path.
*/
function Module(path) {
return new Exports(path);
}
Module.require = Exports.prototype.require;
Module.exports = Module;
Module._paths = paths;
/**
* Reference self as exports
* which also happens to be the constructor
* so you can assign items to the namespace:
*
* //assign to Module.X
* //assume module.exports is Module
* module.exports.X = Foo; //Module.X === Foo;
* Module.exports('foo'); //creates module.exports object.
*
*/
module.exports = Module;
/**
* In the browser assign
* to a global namespace
* obviously 'Module' would
* be whatever your global namespace is.
*/
if (typeof(window) !== 'undefined')
window.Marionette = Module;
}(
this,
(typeof(module) === 'undefined') ?
{} :
module
));

View file

@ -0,0 +1,67 @@
/* 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';
function Command(body) {
this.id = null;
this.name = body.name;
this.params = body.parameters || {};
}
Command.prototype.toString = function() {
return 'Command ' + JSON.stringify({
id: this.id,
name: this.name,
params: this.params
});
};
Command.prototype.toMsg = function() {
if (!this.id) {
throw new RangeError('Command ID must be specified');
}
return [Command.TYPE, this.id, this.name, this.params];
};
Command.fromMsg = function(msg) {
return new Command(msg[1], msg[2], msg[3]);
};
Command.TYPE = 0;
function Response(msgId, obj) {
this.id = msgId;
this.error = null;
this.result = null;
if (obj instanceof Error) {
this.error = obj;
} else {
this.result = obj;
}
}
Response.prototype.toString = function() {
return 'Response ' + JSON.stringify({
id: this.id,
error: this.error,
result: this.result
});
};
Response.prototype.toMsg = function() {
return [Response.TYPE, this.id, this.error, this.result];
};
Response.fromMsg = function(msg) {
return new Response(msg[1], msg[2] || msg[3]);
};
Response.TYPE = 1;
module.exports = {
Command: Command,
Response: Response,
};

View file

@ -0,0 +1,64 @@
/* global Marionette */
(function(module, ns) {
'use strict';
/**
* For a multifinger gesture, we can use MultiActions.
* For example, one finger to hold down
* while the other finger moves from one element to another.
*
* @class Marionette.MultiActions
* @param {Marionette.Client} context of a client.
*/
function MultiActions(client) {
this.client = client;
this.multiActions = [];
this.maxLength = 0;
}
MultiActions.prototype = {
/**
* Adds a action chain for execution.
*
* @method add
* @param {Object} action {{#crossLink "Marionette.Actions"}}{{/crossLink}}.
*
* @return {Object} self.
*/
add: function add(action) {
this.multiActions.push(action.actionChain);
if (action.actionChain.length > this.maxLength) {
this.maxLength = action.actionChain.length;
}
return this;
},
/**
* Send multiple action chains that have been added
* to the server side for execution.
*
* @method perform
* @param {Function} callback callback when the perform completes.
*/
perform: function perform(callback) {
var cmd = {
type: 'multiAction',
value: this.multiActions,
max_length: this.maxLength
};
this.client._sendCommand(cmd, callback);
this.multiActions = [];
return this;
}
};
module.exports = MultiActions;
}.apply(
this,
(this.Marionette) ?
[Marionette('multi-actions'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,212 @@
/* global Marionette */
(function(module, ns) {
'use strict';
/**
* Constructor
*
* @param {Object} list of events to add onto responder.
*/
function Responder(events) {
this._$events = {};
if (typeof(events) !== 'undefined') {
this.addEventListener(events);
}
}
/**
* Stringifies request to websocket
*
*
* @param {String} command command name.
* @param {Object} data object to be sent over the wire.
* @return {String} json object.
*/
Responder.stringify = function stringify(command, data) {
return JSON.stringify([command, data]);
};
/**
* Parses request from WebSocket.
*
* @param {String} json json string to translate.
* @return {Object} ex: { event: 'test', data: {} }.
*/
Responder.parse = function parse(json) {
var data;
try {
data = (json.forEach) ? json : JSON.parse(json);
} catch (e) {
throw new Error('Could not parse json: "' + json + '"');
}
return {event: data[0], data: data[1]};
};
Responder.prototype = {
parse: Responder.parse,
stringify: Responder.stringify,
/**
* Events on this instance
*
* @type Object
*/
_$events: null,
/**
* Recieves json string event and dispatches an event.
*
* @param {String|Object} json data object to respond to.
* @param {String} json.event event to emit.
* @param {Object} json.data data to emit with event.
* @param {Object} [params] option number of params to pass to emit.
* @return {Object} result of WebSocketCommon.parse.
*/
respond: function respond(json) {
var event = Responder.parse(json),
args = Array.prototype.slice.call(arguments).slice(1);
args.unshift(event.data);
args.unshift(event.event);
this.emit.apply(this, args);
return event;
},
//TODO: Extract event emitter logic
/**
* Adds an event listener to this object.
*
*
* @param {String} type event name.
* @param {Function} callback event callback.
*/
addEventListener: function addEventListener(type, callback) {
var event;
if (typeof(callback) === 'undefined' && typeof(type) === 'object') {
for (event in type) {
if (type.hasOwnProperty(event)) {
this.addEventListener(event, type[event]);
}
}
return this;
}
if (!(type in this._$events)) {
this._$events[type] = [];
}
this._$events[type].push(callback);
return this;
},
/**
* Adds an event listener which will
* only fire once and then remove itself.
*
*
* @param {String} type event name.
* @param {Function} callback fired when event is emitted.
*/
once: function once(type, callback) {
var self = this;
function onceCb() {
self.removeEventListener(type, onceCb);
callback.apply(this, arguments);
}
this.addEventListener(type, onceCb);
return this;
},
/**
* Emits an event.
*
* Accepts any number of additional arguments to pass unto
* event listener.
*
* @param {String} eventName name of the event to emit.
* @param {Object} [arguments] additional arguments to pass.
*/
emit: function emit() {
var args = Array.prototype.slice.call(arguments),
event = args.shift(),
eventList,
self = this;
if (event in this._$events) {
eventList = this._$events[event];
eventList.forEach(function(callback) {
callback.apply(self, args);
});
}
return this;
},
/**
* Removes all event listeners for a given event type
*
*
* @param {String} event event type to remove.
*/
removeAllEventListeners: function removeAllEventListeners(name) {
if (name in this._$events) {
//reuse array
this._$events[name].length = 0;
}
return this;
},
/**
* Removes a single event listener from a given event type
* and callback function.
*
*
* @param {String} eventName event name.
* @param {Function} callback same instance of event handler.
*/
removeEventListener: function removeEventListener(name, callback) {
var i, length, events;
if (!(name in this._$events)) {
return false;
}
events = this._$events[name];
for (i = 0, length = events.length; i < length; i++) {
if (events[i] && events[i] === callback) {
events.splice(i, 1);
return true;
}
}
return false;
}
};
Responder.prototype.on = Responder.prototype.addEventListener;
Responder.prototype.removeListener = Responder.prototype.removeEventListener;
module.exports = Responder;
}.apply(
this,
(this.Marionette) ?
[Marionette('responder'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,85 @@
'use strict';
var CommandStream = require('../marionette/command-stream');
var debug = require('debug')('marionette:connection-manager');
/**
* Socket class
*/
ConnectionManger.Socket = require('net').Socket;
/**
* @class
* @constructor
*/
function ConnectionManger() {
this.connections = {};
}
ConnectionManger.prototype = {
currentId: 0,
defaultPort: 2828,
/**
* Opens a connection and returns the id and connection
*
* manager.open()
* // => {id: 0, connection: new CommandStream() }
*
* @this
* @param {Numeric} port port number.
* @return {Object} { id: connectionId, connection: commandStream }.
*/
open: function open(port) {
var socket,
stream,
id = this.currentId++,
closeFn = this.close.bind(this, id);
port = port || this.defaultPort;
socket = new ConnectionManger.Socket();
socket.on('close', closeFn);
socket.on('end', closeFn);
stream = new CommandStream(socket);
socket.on('error', function() {
debug(arguments);
});
socket.on('end', function() {
debug('socket close');
});
socket.connect(port);
this.connections[id] = stream;
return { id: id, connection: stream };
},
/**
* Returns connection by id
*
* @this
* @param {Number} id connection id.
* @return {CommandStream} command stream for connection id.
*/
get: function get(id) {
return this.connections[id];
},
/**
* Closes and removes a connection.
*
* @this
* @param {Number} id connection id to close.
*/
close: function close(id) {
delete this.connections[id];
}
};
module.exports = exports = ConnectionManger;

View file

@ -0,0 +1,20 @@
{
"name": "marionette-client",
"version": "1.9.4",
"author": "The Gaia Team <dev-gaia@lists.mozilla.org>",
"description": "Marionette Javascript Client",
"main": "lib/marionette/index",
"dependencies": {
"debug": "~0.6",
"json-wire-protocol": "file:../json-wire-protocol",
"socket-retry-connect": "file:../socket-retry-connect",
"sockit-to-me": "file:../sockit-to-me"
},
"devDependencies": {
"yuidocjs": "~0.3",
"node-static": "~0.6",
"chai": "~1.7.2"
}
}

View file

@ -0,0 +1,103 @@
'use strict';
var assert = require('chai').assert;
var helper = global.helper = {};
global.assert = assert;
var deepEqual = assert.deepEqual.bind(assert);
assert.deepEqual = function(one, other, msg) {
// Hack so that we pass when comparing
// { '0': foo } with [foo]
deepEqual(argsToArrays(one), argsToArrays(other), msg);
};
function argsToArrays(obj) {
if (typeof obj !== 'object') {
return obj;
}
var proto;
try {
proto = Object.getPrototypeOf(obj);
if (proto && Object.keys(proto).length) {
// This is something fancy...
return obj;
}
} catch (err) {
// uh wtf
return obj;
}
if (!Array.isArray(obj) && isArrayLike(obj)) {
obj = Array.prototype.slice.call(obj);
}
for (var key in obj) {
obj[key] = argsToArrays(obj[key]);
}
return obj;
}
function isArrayLike(obj) {
return Object.keys(obj).every(function(value) {
var representation = parseInt(value);
return typeof representation === 'number' && !isNaN(representation);
});
}
helper.requireLib = function(path) {
return require('../lib/' + path);
};
helper.nsFind = function(obj, string) {
var result = obj;
var parts = string.split('.');
var part;
while ((part = parts.shift())) {
if (!result[part]) {
throw new Error('Cannot find ' + string + ' in object ');
}
result = result[part];
}
return result;
};
helper.require = function(path, component, cb) {
if (typeof(component) === 'function') {
//new module pattern
cb = component;
component = path;
if (/^support/.test(path)) {
path = '/test/' + path;
} else {
path = '/lib/marionette/' + path;
}
} else {
//old system
path = '/lib/' + path;
}
cb(require('..' + path));
};
helper.require('responder', function(obj) {});
helper.require('example-commands', function(obj) {
global.exampleCmds = obj;
});
helper.require('support/device-interaction', function(obj) {
global.DeviceInteraction = obj;
});
helper.require('support/socket', function(obj) {
global.FakeSocket = obj;
});
helper.require('support/mock-driver', function(obj) {
global.MockDriver = obj;
});

View file

@ -0,0 +1,71 @@
/* global DeviceInteraction, exampleCmds, helper */
'use strict';
require('../helper');
suite('scope', function() {
var client = marionette.client(),
Actions,
device,
subject;
helper.require('actions', function(obj) {
Actions = obj;
});
device = new DeviceInteraction(exampleCmds, function() {
return subject;
});
suite('actions', function() {
setup(function() {
subject = new Actions(client);
});
test('can move the page', function() {
var bodyElement;
client.goUrl('http://yahoo.com');
client.waitFor(function() {
client.findElement('body', function(error, element) {
if (error) {
console.log('cannot find');
return;
}
bodyElement = element;
});
if (bodyElement !== undefined) {
return true;
}
});
subject.
press(bodyElement, 100, 100).
moveByOffset(0, 10).
perform();
// TODO: Bug 965385 - Use `Actions#wait` in integration tests
// Model this pause with an invocation of `Actions#wait` once "Bug 962645
// - Action chains only handle one press->release chain" is resolved.
client.executeAsyncScript(function() {
setTimeout(marionetteScriptFinished, 50);
});
subject.
release().
perform();
// TODO: Bug 965385 - Use `Actions#wait` in integration tests
// Model this pause with an invocation of `Actions#wait` once "Bug 962645
// - Action chains only handle one press->release chain" is resolved.
client.executeAsyncScript(function() {
setTimeout(marionetteScriptFinished, 50);
});
subject.
flick(bodyElement, 0, 0, 0, 200).
perform();
});
});
});

View file

@ -0,0 +1,29 @@
/* global marionette, setup */
'use strict';
var querystring = require('querystring');
suite('client methods', function() {
var client = marionette.client();
setup(function() {
client.goUrl('data:text/html,' + querystring.escape(
'<div id="host"></div>'));
client.executeScript(function() {
var host = document.getElementById('host');
var root = host.createShadowRoot();
var button = document.createElement('button');
button.id = 'button';
button.textContent = 'Foo';
root.appendChild(button);
});
});
test('#switchToShadowRoot', function() {
var host = client.findElement('#host');
client.switchToShadowRoot(host);
var button = client.findElement('#button');
button.click();
});
});

View file

@ -0,0 +1,18 @@
/* global assert */
'use strict';
require('../../helper');
suite('drivers/tcp-sync', function() {
var client = marionette.client();
test('can execute sync commands', function() {
client.goUrl('http://yahoo.com');
client.goUrl('http://google.com');
client.goUrl('http://yahoo.com');
var location = client.executeScript(function() {
return window.location.href;
});
assert.ok(location.indexOf('yahoo.com') !== -1);
});
});

View file

@ -0,0 +1,84 @@
/* global assert */
'use strict';
var querystring = require('querystring');
require('../helper');
suite('element methods', function() {
var client = marionette.client();
setup(function() {
client.goUrl('data:text/html,' + querystring.escape(
'<div id="el" style="width: 50px; height: 50px">cheese</div>'));
});
test('#displayed', function() {
var element = client.findElement('html');
assert.strictEqual(element.displayed(), true);
});
test('#scriptWith', function() {
var element = client.findElement('html');
var evaled = element.scriptWith(function(el, arg) {
return el.tagName + ' ' + arg;
}, ['FTW!']);
assert.equal(evaled, 'HTML FTW!');
});
test('#findElement', function() {
var el = client.findElement('html');
assert.instanceOf(el, client.Element);
});
test('#findElements', function() {
var els = client.findElements('html');
assert.isArray(els);
els.forEach(function(el) { assert.instanceOf(el, client.Element); });
});
test('#findElement - missing', function() {
var err;
try {
client.findElement('#fooobaramazingmissing');
} catch (e) {
err = e;
}
if (!err) throw new Error('missing element did not trigger an error');
assert.equal(err.type, 'NoSuchElement');
});
test('#cssProperty', function() {
var body = client.findElement('body');
var font = body.cssProperty('font-size');
assert.ok(font, 'returns a css property value');
});
test('#size', function () {
var element = client.findElement('#el');
var size = element.size();
assert.property(size, 'width');
assert.property(size, 'height');
assert.equal(size.width, 50);
assert.equal(size.height, 50);
});
test('#location', function () {
var element = client.findElement('#el');
var location = element.location();
assert.property(location, 'x');
assert.property(location, 'y');
});
test('#rect', function () {
var element = client.findElement('#el');
var rect = element.rect();
assert.property(rect, 'x');
assert.property(rect, 'y');
assert.property(rect, 'width');
assert.property(rect, 'height');
assert.equal(rect.width, 50);
assert.equal(rect.height, 50);
});
});

View file

@ -0,0 +1,43 @@
/* global assert, helper */
'use strict';
require('../helper');
suite('error handling', function() {
var MarionetteError;
helper.require('error', function(obj) {
MarionetteError = obj;
});
var client = marionette.client();
test('catch error', function() {
try {
client.executeScript(function() {
throw new Error('e');
});
} catch (e) {
e.client.screenshot();
}
});
test('throws no such element error', function() {
try {
client.findElement('cheese');
assert.fail('expected no such element error to be thrown');
} catch (e) {
assert.include(e.toString(), 'NoSuchElement');
assert.property(e, 'type');
assert.equal(e.type, 'NoSuchElement');
}
});
test('throws javascript error', function() {
try {
client.executeScript(function() { throw Error('rochefort'); });
} catch (e) {
assert.include(e.toString(), 'JavaScriptError');
assert.property(e, 'type');
assert.equal(e.type, 'JavaScriptError');
}
});
});

View file

@ -0,0 +1,18 @@
'use strict';
module.exports.skipInitialError = function (client) {
setup(function(done) {
client.executeAsyncScript(function() {
setTimeout(marionetteScriptFinished, 10);
}, function(err) {
// hacked work around for gaia exception at startup.
if (
err &&
err.message &&
err.message.indexOf('Expected IccHelper') !== -1
) {
return done();
}
done(err);
});
});
};

View file

@ -0,0 +1,36 @@
/* global assert */
'use strict';
require('../helper');
suite('scope', function() {
var client = marionette.client(),
helper = require('./helper'),
timeoutA,
timeoutB;
helper.skipInitialError(client);
setup(function() {
timeoutA = client.scope({ scriptTimeout: 1000 });
timeoutB = client.scope({ scriptTimeout: 250 });
});
test('should handle scope switching', function() {
function sleep() {
setTimeout(function() {
marionetteScriptFinished();
}, 600);
}
timeoutA.executeAsyncScript(sleep);
var err;
try {
timeoutB.executeAsyncScript(sleep);
} catch (e) {
err = e;
}
// sleep throws error on shorter timeout
assert.ok(err);
});
});

View file

@ -0,0 +1,95 @@
/* global assert */
'use strict';
require('../helper');
suite('scope', function() {
var client = marionette.client(),
helper = require('./helper');
helper.skipInitialError(client);
suite('sync waitFor', function() {
test('should not yield in sync code', function() {
var tries = 0;
var succeed = 10;
// short signature
client.waitFor(function() {
return (++tries === succeed);
});
assert.strictEqual(tries, 10);
});
test('should eventually timeout', function() {
var timeout = 450;
var start = Date.now();
var err;
try {
client.waitFor(function() {
return false;
}, { timeout: timeout });
} catch (e) {
err = e;
}
assert.ok(err);
assert.operator(Date.now() - start, '>=', timeout);
});
test('should do the mocha sync', function() {
var tries = 0, success = 3;
client.waitFor(function fn(waitForComplete) {
tries += 1;
if (tries === success) {
waitForComplete(null, true);
}
setTimeout(fn.bind(this, waitForComplete), 10);
});
assert.strictEqual(tries, success);
});
test('should do the mocha fail sync', function(done) {
var err = new Error('failure!');
try {
client.waitFor(function(waitForComplete) {
waitForComplete(err);
});
} catch (e) {
assert.strictEqual(e, err);
done();
}
});
});
suite('callback style wait for calls', function() {
test('should throw an error when given', function() {
var err = new Error('failure!');
var isSync = false;
try {
client.waitFor(function(waitForComplete) {
waitForComplete(err);
}, function(e) {
assert.strictEqual(e, err);
isSync = true;
});
} catch(e) {
assert.equal(e, err);
}
assert.ok(isSync, 'callback style is sync');
});
test('should fire when async condition is met', function() {
var tries = 0;
var success = 3;
client.waitFor(function() {
return ++tries === success;
}, function() {
assert.strictEqual(tries, 3);
});
assert.equal(tries, 3, 'is sync');
});
});
});

View file

@ -0,0 +1 @@
--profile-base profile.js

View file

@ -0,0 +1,225 @@
/* global DeviceInteraction, MockDriver, exampleCmds, assert, helper */
'use strict';
suite('marionette/actions', function() {
var driver, subject, client, device,
Actions, Client;
helper.require('actions', function(obj) {
Actions = obj;
});
helper.require('client', function(obj) {
Client = obj;
});
device = new DeviceInteraction(exampleCmds, function() {
return subject;
});
setup(function() {
driver = new MockDriver();
client = new Client(driver);
subject = new Actions(client);
});
suite('initialization', function() {
test('should set client', function() {
assert.strictEqual(subject.client, client);
});
});
suite('.press', function() {
var element, x, y;
setup(function() {
element = { id: '{fake-uuid-root}' };
x = 0;
y = 0;
subject.press(element, x, y);
});
test('should have a press action in the chain', function() {
var pressAction = [['press', element.id, x, y]];
assert.deepEqual(subject.actionChain, pressAction);
});
});
suite('.release', function() {
setup(function() {
subject.release();
});
test('should have a release action in the chain', function() {
var releaseAction = [['release']];
assert.deepEqual(subject.actionChain, releaseAction);
});
});
suite('.move', function() {
var element;
setup(function() {
element = { id: '{fake-uuid-root}' };
subject.move(element);
});
test('should have a move action in the chain', function() {
var moveAction = [['move', element.id]];
assert.deepEqual(subject.actionChain, moveAction);
});
});
suite('.moveByOffset', function() {
var x, y;
setup(function() {
x = 1;
y = 1;
subject.moveByOffset(x, y);
});
test('should have a move by offset action in the chain', function() {
var moveByOffsetAction = [['moveByOffset', x, y]];
assert.deepEqual(subject.actionChain, moveByOffsetAction);
});
});
suite('.wait', function() {
var time;
setup(function() {
time = 1;
subject.wait(time);
});
test('should have a wait action in the chain', function() {
var waitAction = [['wait', time]];
assert.deepEqual(subject.actionChain, waitAction);
});
});
suite('.cancel', function() {
setup(function() {
subject.cancel();
});
test('should have a cancel action in the chain', function() {
var cancelAction = [['cancel']];
assert.deepEqual(subject.actionChain, cancelAction);
});
});
suite('.tap', function() {
var element, x, y;
setup(function() {
element = { id: '{fake-uuid-root}' };
x = 0;
y = 0;
subject.tap(element, x, y);
});
test('should have a tap action in the chain', function() {
var tapAction = [
['press', element.id, x, y],
['release']
];
assert.deepEqual(subject.actionChain, tapAction);
});
});
suite('.doubleTap', function() {
var element, x, y;
setup(function() {
element = { id: '{fake-uuid-root}' };
x = 0;
y = 0;
subject.doubleTap(element, x, y);
});
test('should have a double tap action in the chain', function() {
var doubleTapAction = [
['press', element.id, x, y],
['release'],
['press', element.id, x, y],
['release']
];
assert.deepEqual(subject.actionChain, doubleTapAction);
});
});
suite('.flick', function() {
var element, x1, y1, x2, y2;
setup(function() {
element = { id: '{fake-uuid-root}' };
x1 = 0;
y1 = 0;
x2 = 100;
y2 = 100;
});
function shouldHaveFlickAction() {
var firstAction = ['press', element.id, x1, y1];
var lastAction = ['release'];
var lastActionIndex = subject.actionChain.length - 1;
assert.deepEqual(subject.actionChain[0], firstAction);
for (var i = 1; i < lastActionIndex; i += 2) {
assert.strictEqual(subject.actionChain[i][0], 'moveByOffset');
assert.strictEqual(subject.actionChain[i + 1][0], 'wait');
}
assert.deepEqual(subject.actionChain[lastActionIndex], lastAction);
}
test('should have a flick action in the chain', function() {
subject.flick(element, x1, y1, x2, y2);
shouldHaveFlickAction();
});
test('should have a flick action in the chain ' +
'when the duration param is 300', function() {
var duration = 300;
subject.flick(element, x1, y1, x2, y2, duration);
shouldHaveFlickAction();
});
});
suite('.longPress', function() {
var element, time;
setup(function() {
element = { id: '{fake-uuid-root}' };
time = 1;
subject.longPress(element, time);
});
test('should have a long press action in the chain', function() {
var longPressAction = [
['press', element.id],
['wait', time],
['release']
];
assert.deepEqual(subject.actionChain, longPressAction);
});
});
suite('.perform', function() {
device.
issues('perform').
shouldSend({
name: 'actionChain',
parameters: {
chain: [],
nextId: null
}
}).
serverResponds('value').
callbackReceives('value');
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,213 @@
/* global assert, helper */
'use strict';
suite('marionette/command-stream', function() {
var subject, socket,
Responder,
CommandStream;
helper.require('responder', function(obj) {
Responder = obj;
});
helper.require('command-stream', function(obj) {
CommandStream = obj;
});
setup(function() {
socket = new Responder();
subject = new CommandStream(socket);
});
suite('initialization', function() {
test('should save socket', function() {
assert.strictEqual(subject.socket, socket);
});
test('should be an event emitter', function() {
assert.instanceOf(subject.on, Function);
});
});
suite('socket event: data', function() {
var calledWith, data;
setup(function() {
data = {};
subject.on('command', function() {
calledWith = Array.prototype.slice.call(arguments);
});
subject.socket.emit('data', new Buffer(subject.stringify(data)));
});
test('should call add', function() {
assert.deepEqual(calledWith, [data]);
});
});
suite('.stringify', function() {
var command = {works: true},
result;
function shouldStringify() {
test('should return json (n:json)', function() {
var string = JSON.stringify(command);
assert.strictEqual(result, String(string.length) + ':' + string);
});
}
suite('when given an object', function() {
setup(function() {
result = subject.stringify(command);
});
shouldStringify();
});
suite('when given a string', function() {
setup(function() {
command = 'some random string';
result = subject.stringify(command);
});
shouldStringify();
});
});
suite('.send', function() {
var calledWith = [], data = {uniq: true};
function sendsToSocket(fnName) {
setup(function() {
subject.socket[fnName] = function() {
calledWith = Array.prototype.slice.call(arguments);
};
subject.send(data);
});
test('should write to socket', function() {
assert.strictEqual(calledWith[0], subject.stringify(data));
});
}
suite('when using socket.send', function() {
sendsToSocket('send');
});
suite('when using socket.write', function() {
sendsToSocket('write');
test('should write in utf8', function() {
assert.strictEqual(calledWith[1], 'utf8');
});
});
});
suite('.add', function() {
var commands = [],
chunks = {},
commandList = {
success: {'from': 'someX'},
fail: {'from': 'zya'}
};
function add(string, log) {
var buffer = new Buffer(string);
subject.add(buffer);
}
function emitsCommand(name, index) {
index = index || 0;
name = name || 'success';
test('should emit ' + name + ' command at #' + index + '', function() {
assert.deepEqual(commands[index], commandList[name]);
});
}
function hasCommands(number) {
test('should have ' + String(number) + ' of commands', function() {
assert.strictEqual(commands.length, number);
});
}
setup(function() {
commands = [];
Object.keys(commandList).forEach(function(key) {
chunks[key] = subject.stringify(commandList[key]);
});
subject.on(subject.commandEvent, function(response) {
commands.push(response);
});
});
suite('when given the entire command', function() {
setup(function() {
add(chunks.success);
});
emitsCommand('success', 0);
hasCommands(1);
});
suite('when given single command in multiple chunks', function() {
setup(function() {
add(chunks.success.slice(0, 1));
add(chunks.success.slice(1, 2));
add(chunks.success.slice(2));
});
emitsCommand('success', 0);
hasCommands(1);
});
suite('when given multiple commands in two chunks', function() {
setup(function() {
var chunk = chunks.success + chunks.fail,
piece1,
piece2,
half = Math.floor(chunk.length / 2);
piece1 = chunk.slice(0, half);
piece2 = chunk.slice(half);
//sanity check
assert.strictEqual(piece1 + piece2, chunk);
add(piece1);
add(piece2);
});
emitsCommand('success', 0);
emitsCommand('fail', 1);
hasCommands(2);
});
suite('when given both commands in one chunk', function() {
setup(function() {
add(chunks.fail + chunks.success);
});
emitsCommand('fail', 0);
emitsCommand('success', 1);
hasCommands(2);
});
});
});

View file

@ -0,0 +1,358 @@
/* global assert, exampleCmds, helper */
'use strict';
suite('marionette/drivers/abstract', function() {
var subject,
Backend,
sent = [];
helper.require('drivers/abstract', function(obj) {
Backend = obj;
});
setup(function() {
subject = new Backend();
sent = [];
subject._sendCommand = function() {
sent.push(arguments);
};
});
suite('initialization', function() {
test('should setup ._sendQueue', function() {
assert.deepEqual(subject._sendQueue, []);
});
test('should not be ready', function() {
assert.strictEqual(subject.ready, false);
});
test('should setup ._responseQueue', function() {
assert.instanceOf(subject._responseQueue, Array);
});
test('should have timeout set to 10000', function() {
assert.strictEqual(subject.timeout, 10000);
});
test('should be _waiting', function() {
assert.strictEqual(subject._waiting, true);
});
});
suite('event: device response', function() {
var callback,
callbackResponse,
response;
setup(function() {
callbackResponse = null;
response = {
id: 10,
response: {
from: 'marionette',
value: 'hit'
}
};
//emulate connect
subject.connectionId = 10;
subject.ready = true;
subject._waiting = false;
callback = function() {
callbackResponse = arguments;
};
subject.send(exampleCmds.newSession(), callback);
assert.strictEqual(subject._waiting, true);
});
suite('when response is for different device', function() {
setup(function() {
subject.connectionId = 101;
subject._onDeviceResponse(response);
});
test('should trigger response callbacks', function() {
assert.strictEqual(callbackResponse, null);
});
});
suite('when response is for device id', function() {
var calledNext;
setup(function() {
calledNext = false;
subject._nextCommand = function() {
calledNext = true;
Backend.prototype._nextCommand.apply(this, arguments);
};
subject._onDeviceResponse(response);
});
test('should trigger response callbacks', function() {
assert.deepEqual(callbackResponse[0], response.response);
});
test('should clear response queue', function() {
assert.strictEqual(subject._responseQueue.length, 0);
});
test('should not be waiting', function() {
assert.strictEqual(subject._waiting, false);
});
});
});
suite('.close', function() {
var calledClose;
setup(function() {
calledClose = false;
subject.ready = true;
subject._responseQueue = [function() {}];
subject._close = function() {
calledClose = true;
};
subject.close();
});
test('should call _close', function() {
assert.strictEqual(calledClose, true);
});
test('should not be ready', function() {
assert.strictEqual(subject.ready, false);
});
test('should clean up _responseQueue', function() {
assert.strictEqual(subject._responseQueue.length, 0);
});
});
suite('.connect with protocol 1', function() {
var cmd, calledChild;
setup(function(done) {
cmd = exampleCmds.connectProto1();
calledChild = false;
subject._connect = function() {
subject.connectionId = 10;
calledChild = true;
//this will cause connect to callback to fire
subject._onDeviceResponse({
id: 10,
response: cmd
});
};
assert.strictEqual(subject._waiting, true);
subject.connect(function() {
done();
});
});
test('should set .marionetteProtocol', function() {
assert.property(subject, 'marionetteProtocol');
assert.strictEqual(subject.marionetteProtocol, 1 /* fallback */);
});
test('should set .applicationType', function() {
assert.property(subject, 'applicationType');
assert.strictEqual(subject.applicationType, cmd.applicationType);
});
test('should set .traits', function() {
assert.property(subject, 'traits');
assert.strictEqual(subject.traits, cmd.traits);
});
test('should call _connect', function() {
assert.strictEqual(calledChild, true);
});
test('should not be waiting', function() {
assert.strictEqual(subject._waiting, false);
});
test('should be ready', function() {
assert.strictEqual(subject.ready, true);
});
});
suite('.connect with protocol 2', function() {
var cmd, calledChild;
setup(function(done) {
cmd = exampleCmds.connectProto2();
calledChild = false;
subject._connect = function() {
subject.connectionId = 10;
calledChild = true;
//this will cause connect to callback to fire
subject._onDeviceResponse({
id: 10,
response: cmd
});
};
assert.strictEqual(subject._waiting, true);
subject.connect(function() {
done();
});
});
test('should set .marionetteProtocol', function() {
assert.property(subject, 'marionetteProtocol');
assert.strictEqual(subject.marionetteProtocol, cmd.marionetteProtocol);
});
test('should set .applicationType', function() {
assert.property(subject, 'applicationType');
assert.strictEqual(subject.applicationType, cmd.applicationType);
});
test('should call _connect', function() {
assert.strictEqual(calledChild, true);
});
test('should not be waiting', function() {
assert.strictEqual(subject._waiting, false);
});
test('should be ready', function() {
assert.strictEqual(subject.ready, true);
});
});
suite('._nextCommand', function() {
var cmd1, cmd2;
setup(function() {
cmd1 = exampleCmds.newSession();
cmd2 = exampleCmds.newSession({isOther: true});
subject._sendQueue[0] = cmd1;
subject._sendQueue[1] = cmd2;
});
suite('when waiting', function() {
setup(function() {
subject._waiting = true;
subject._nextCommand();
});
test('should not send command to server', function() {
assert.strictEqual(sent.length, 0);
});
});
suite('when not waiting', function() {
setup(function() {
subject._waiting = false;
subject._nextCommand();
});
test('should be waiting', function() {
assert.strictEqual(subject._waiting, true);
});
test('should send command to server', function() {
assert.deepEqual(sent[0][0], cmd1);
});
});
suite('when there are no comamnds and we are not waiting', function() {
setup(function() {
//emulate connect
subject._waiting = false;
subject._responseQueue = [];
subject._sendQueue = [];
subject._nextCommand();
});
test('should not be waiting', function() {
assert.strictEqual(subject._waiting, false);
});
});
});
suite('.send', function() {
var cmd, cb = function() {};
setup(function() {
cmd = exampleCmds.newSession();
});
suite('when device is not ready', function() {
test('should throw an error', function() {
assert.throws(function() {
subject.send({ type: 'newSession' });
}, /not ready/);
});
});
suite('when not waiting for a response', function() {
setup(function() {
//emulate connect
subject.ready = true;
subject._waiting = false;
subject.send(cmd, cb);
});
test('should send command', function() {
assert.strictEqual(sent.length, 1);
});
test('should be waiting', function() {
assert.strictEqual(subject._waiting, true);
});
});
suite('when waiting for a response', function() {
var nextCalled;
setup(function() {
subject.ready = true;
subject._waiting = true;
nextCalled = false;
subject._nextCommand = function() {
nextCalled = true;
};
subject.send(cmd, cb);
});
test('should call next', function() {
assert.strictEqual(nextCalled, true);
});
test('should add send command to queue', function() {
assert.strictEqual(subject._sendQueue[0], cmd);
});
test('should add calback to response queue', function() {
assert.strictEqual(subject._responseQueue[0], cb);
});
});
});
});

View file

@ -0,0 +1,33 @@
'use strict';
var debug = require('debug')('marionette:tcp-sync-test-server');
var net = require('net');
var response = '54:{"applicationType": "gecko", "marionetteProtocol": 2}';
var server = net.createServer(function(connection) { //'connection' listener
debug('server connected');
connection.on('end', function() {
debug('server disconnected');
});
connection.on('error', function(e) {
debug('error', e);
});
connection.write(response);
connection.pipe(connection);
});
function runServer(port) {
server.listen(port, function() { //'listening' listener
debug('server bound');
});
}
if (require.main === module) {
try {
runServer(1234);
}
catch(e) {
}
}

View file

@ -0,0 +1,34 @@
'use strict';
var fork = require('child_process').fork;
suite('marionette/drivers/tcp-sync', function() {
var child = null;
teardown(function() {
if (child) {
child.kill();
child = null;
}
});
test('can wait for marionette socket', function(done) {
this.timeout(5000);
var TcpSync =
require(__dirname + '/../../../lib/marionette/drivers/tcp-sync');
var tcpSync = new TcpSync({port: 1234, host: 'localhost'});
tcpSync.waitForSocket( {timeout: 3000} , function() {
done();
});
setTimeout(function () {
fork(__dirname + '/tcp-sync-test-server.js');
}, 1000);
});
});

View file

@ -0,0 +1,155 @@
/* global assert, helper */
'use strict';
suite('marionette/drivers/tcp', function() {
if (typeof(window) !== 'undefined') {
return;
}
var Abstract,
Driver = require('../../../lib/marionette/drivers/tcp'),
FakeSocket = require('../../support/socket');
helper.require(
'marionette/drivers/abstract',
'Marionette.Drivers.Abstract', function(obj) {
Abstract = obj;
}
);
function issueFirstResponse() {
subject._onDeviceResponse({
id: subject.connectionId,
response: {}
});
}
var subject,
RealSocket,
sockets = [];
var socketRetry = require('socket-retry-connect');
var realWaitForSocket;
setup(function() {
realWaitForSocket = socketRetry.waitForSocket;
RealSocket = Driver.Socket;
Driver.Socket = FakeSocket;
FakeSocket.sockets = sockets;
socketRetry.waitForSocket = function(options, callback) {
var socket = new FakeSocket(options.port);
callback(null, socket);
};
});
teardown(function() {
socketRetry.waitForSocket = realWaitForSocket;
Driver.Socket = RealSocket;
});
setup(function() {
subject = new Driver();
});
test('should accept port and host', function() {
var subject = new Driver({
port: 8888,
host: 'foobar'
});
assert.strictEqual(subject.port, 8888);
assert.strictEqual(subject.host, 'foobar');
});
suite('._sendCommand', function() {
var sent = [];
setup(function(done) {
subject.connect(function() {
subject.client.send = function() {
sent.push(arguments);
};
subject.client.send({
type: 'foo',
});
done();
});
// issue first response so connect will fire
issueFirstResponse();
});
test('should send request to socket', function() {
assert.deepEqual(sent, [
[{type: 'foo'}]
]);
});
});
// TODO(gaye): What is going on here / what is emtest()?
suite.skip('client event: command', function() {
var sent = [];
setup(function() {
sent.length = 0;
subject._onDeviceResponse = function() {
sent.push(arguments);
};
subject._connect();
subject.client.emtest('command', { type: 'foo' });
});
test('should call onDeviceResponse', function() {
assert.deepEqual(sent, [
[{ id: 0, response: {type: 'foo'} }]
]);
});
});
suite('._connect', function() {
suite('retrying', function() {
var net = require('net');
var port = 60066;
setup(function() {
Driver.Socket = RealSocket;
socketRetry.waitForSocket = realWaitForSocket;
subject = new Driver({ port: port });
});
test('should eventually connect', function(done) {
subject._connect();
setTimeout(function() {
var server = net.createServer(function(socket) {
server.close();
done();
}).listen(port);
}, 50);
});
});
});
suite('._close', function() {
setup(function(done) {
subject.connect(function() {
subject.close();
done();
});
issueFirstResponse();
});
test('should close socket', function() {
assert.strictEqual(subject.socket.destroyed, true);
});
});
});

View file

@ -0,0 +1,283 @@
/* global DeviceInteraction, MockDriver, assert, exampleCmds, helper */
'use strict';
// this is hack to ensure device interactions are loaded
suite('marionette/element', function() {
var driver, subject, client, id, device,
Element, Client;
helper.require('element', function(obj) {
Element = obj;
});
helper.require('client', function(obj) {
Client = obj;
});
id = '{fake-uuid-root}';
function simpleCommand(method, type, response, key) {
suite('.' + method, function() {
device.
issues(method).
shouldSend({
name: type,
parameters: {
id: id
}
}).
serverResponds(response).
callbackReceives(key);
});
}
device = new DeviceInteraction(exampleCmds, function() {
return subject;
});
setup(function() {
driver = new MockDriver();
client = new Client(driver);
subject = new Element(id, client);
});
suite('initialization', function() {
test('should set id', function() {
assert.strictEqual(subject.id, id);
});
test('should set client', function() {
assert.strictEqual(subject.client, client);
});
});
suite('._sendCommand', function() {
device.
issues('_sendCommand', {name: 'test'}).
shouldSend({name: 'test', parameters: {id: id}}).
serverResponds('value').
callbackReceives('value');
});
suite('.findElement', function() {
device.
issues('findElement', '#id').
shouldSend({
name: 'findElement',
parameters: {
value: '#id',
element: id,
using: 'css selector'
}
}).
serverResponds('findElementResponse');
test('should send callback a single element', function() {
var value = device.commandCallback.value;
var resultId = exampleCmds.findElementResponse().value;
assert.instanceOf(value, Element);
assert.strictEqual(value.id, resultId);
});
});
suite('.findElements', function() {
device.
issues('findElements', '#id').
shouldSend({
name: 'findElements',
parameters: {
value: '#id',
element: id,
using: 'css selector'
}
}).
serverResponds('findElementsResponse');
test('should send callback an element instance', function() {
var map = device.commandCallback.value.map(function(el) {
return el.id;
});
assert.deepEqual(map, exampleCmds.findElementsResponse().value);
});
});
suite('.scriptWith', function() {
var calledWith,
fn = function() {},
cb = function() {};
setup(function() {
subject.client.executeScript = function() {
calledWith = arguments;
};
});
test('should call client.executeScript with ' +
'element as argument', function() {
subject.scriptWith(fn);
assert.strictEqual(calledWith[0], fn);
assert.deepEqual(calledWith[1], [
subject
]);
assert.strictEqual(calledWith[2], undefined);
});
test('should call client.executeScript with element ' +
' + callback', function(){
subject.scriptWith(fn, cb);
assert.strictEqual(calledWith[0], fn);
assert.deepEqual(calledWith[1], [
subject
]);
assert.strictEqual(calledWith[2], cb);
});
test('should call client.executeScript with element ' +
' + args + callback', function(){
subject.scriptWith(fn, ['foo', 'bar'], cb);
assert.strictEqual(calledWith[0], fn);
assert.deepEqual(calledWith[1], [
subject, 'foo', 'bar'
]);
assert.strictEqual(calledWith[2], cb);
});
});
suite('.equals', function() {
var equals;
var notEquals;
setup(function() {
equals = new Element(id, client);
notEquals = new Element('___I_AM_TITAN', client);
});
test('equals', function() {
assert.isTrue(subject.equals(equals));
});
test('not equal', function() {
assert.isFalse(subject.equals(notEquals));
});
});
suite('.getAttribute', function() {
var attr = 'name';
device.
issues('getAttribute', attr).
shouldSend({
name: 'getElementAttribute',
parameters: {
name: attr,
id: id
}
}).
serverResponds('value').
callbackReceives('value');
});
suite('.sendKeys', function() {
suite('when given a array', function() {
var input = ['f', 'o', 'o'];
device.
issues('sendKeys', input).
shouldSend({
name: 'sendKeysToElement',
parameters: {
value: input,
id: id
}
}).
serverResponds('ok').
callbackReceives();
});
suite('when given a string', function() {
var msg = 'foo';
device.
issues('sendKeys', msg).
shouldSend({
name: 'sendKeysToElement',
parameters: {
value: [msg],
id: id
}
}).
serverResponds('ok').
callbackReceives();
});
});
suite('.cssProperty', function() {
var property = 'margin';
device.
issues('cssProperty', property).
shouldSend({
name: 'getElementValueOfCssProperty',
parameters: {
propertyName: property,
id: id
}
}).
serverResponds('value').
callbackReceives('value');
});
suite('.tap', function() {
suite('when given the x and y offsets', function() {
var x = 10;
var y = 15;
device.
issues('tap', x, y).
shouldSend({
name: 'singleTap',
parameters: {
x: x,
y: y,
id: id
}
}).
serverResponds('value').
callbackReceives('value');
});
suite('when given the x offset only', function() {
var x = 10;
device.
issues('tap', x).
shouldSend({
name: 'singleTap',
parameters: {
x: x,
id: id
}
}).
serverResponds('value').
callbackReceives('value');
});
suite('when no x and y offsets', function() {
simpleCommand('tap', 'singleTap', 'value');
});
});
simpleCommand('tagName', 'getElementTagName', 'value', 'value');
simpleCommand('click', 'clickElement', 'ok');
simpleCommand('text', 'getElementText', 'value', 'value');
simpleCommand('clear', 'clearElement', 'ok');
simpleCommand('selected', 'isElementSelected', 'value', 'value');
simpleCommand('enabled', 'isElementEnabled', 'value', 'value');
simpleCommand('displayed', 'isElementDisplayed', 'value', 'value');
simpleCommand('size', 'getElementRect', 'value', 'value');
simpleCommand('location', 'getElementRect', 'value', 'value');
simpleCommand('rect', 'getElementRect', 'value', 'value');
});

View file

@ -0,0 +1,80 @@
/* global assert, helper */
'use strict';
suite('marionette/error', function() {
var MarionetteError;
helper.require('error', function(obj) {
MarionetteError = obj;
});
test('should expose .ERRORS', function() {
assert.property(MarionetteError, 'ERRORS');
});
suite('#error', function() {
test('should populate message', function() {
var err = new MarionetteError({}, {message: 'msg'});
assert.property(err, 'message');
assert.include(err.message, 'msg');
});
test('should populate stacktrace', function() {
var trace = 'brie';
var err = new MarionetteError({}, {stacktrace: trace});
assert.property(err, 'stack');
assert.include(err.message, trace);
assert.include(err.stack, trace);
});
test('should populate type', function() {
var err = new MarionetteError({}, {});
assert.property(err, 'type');
});
test('should recognise known error code', function() {
var err = new MarionetteError({}, {error: 'no such element'});
assert.property(err, 'type');
assert.equal(err.type, 'NoSuchElement');
});
test('should fall back to GenericError for unknown error code', function() {
var err = new MarionetteError({}, {error: 'brunost'});
assert.property(err, 'type');
assert.equal(err.type, 'GenericError');
});
test('should support all errors', function() {
for (var s in MarionetteError.ERRORS) {
var err = new MarionetteError({}, {error: s});
assert.strictEqual(err.type, MarionetteError.ERRORS[s]);
assert.include(err.message, MarionetteError.ERRORS[s]);
assert.instanceOf(err, Error);
}
});
test('should contain client', function() {
var client = {};
var result = new MarionetteError(client, {
status: 7777,
message: 'foo',
stack: 'bar'
});
assert.equal(result.client, client);
assert.strictEqual(
result.name,
MarionetteError.ERRORS['webdriver error']
);
});
test('should use GenericError error when unknown error code', function() {
var result = new MarionetteError({}, {error: 'cheese'});
assert.strictEqual(
result.name,
MarionetteError.ERRORS['webdriver error']
);
});
});
});

View file

@ -0,0 +1,32 @@
/* global assert, helper */
'use strict';
suite('marionette/index', function() {
var Index;
suiteSetup(function() {
Index = helper.requireLib('marionette/index');
});
test('should have paths', function() {
assert.instanceOf(Index.Element, Object);
assert.instanceOf(Index.Client, Object);
assert.instanceOf(Index.Drivers, Object);
assert.instanceOf(Index.CommandStream, Object);
assert.instanceOf(Index.Drivers.Abstract, Object);
if (typeof(window) === 'undefined') {
assert.instanceOf(Index.Drivers.Tcp, Object);
} else {
try {
if (typeof(window.navigator.mozTCPSocket) !== 'undefined') {
assert.instanceOf(Index.Drivers.MozTcp, Object);
}
} catch(e) {
}
}
});
});

View file

@ -0,0 +1,57 @@
/* global DeviceInteraction, MockDriver, assert, exampleCmds, helper */
'use strict';
suite('marionette/multi-actions', function() {
var driver, subject, actions, client, device,
MultiActions, Actions, Client;
helper.require('multi-actions', function(obj) {
MultiActions = obj;
});
helper.require('actions', function(obj) {
Actions = obj;
});
helper.require('client', function(obj) {
Client = obj;
});
device = new DeviceInteraction(exampleCmds, function() {
return subject;
});
setup(function() {
driver = new MockDriver();
client = new Client(driver);
actions = new Actions(client);
subject = new MultiActions(client);
});
suite('initialization', function() {
test('should set client', function() {
assert.strictEqual(subject.client, client);
});
});
suite('.add', function() {
setup(function() {
subject.add(actions);
});
test('should have a action chain', function() {
assert.deepEqual(subject.multiActions[0], actions.actionChain);
});
});
suite('.perform', function() {
device.
issues('perform').
shouldSend({
type: 'multiAction',
value: [],
max_length: 0
}).
serverResponds('ok').
callbackReceives();
});
});

View file

@ -0,0 +1,98 @@
/* global assert */
'use strict';
var FakeSocket = require('../support/socket'),
CommandStream = require('../../lib/marionette/command-stream'),
ConnectionManger = require('../../lib/node/connection-manager');
suite('connection-manager', function() {
var RealSocket, subject, sockets = [];
suiteSetup(function() {
RealSocket = ConnectionManger.Socket;
ConnectionManger.Socket = FakeSocket;
FakeSocket.sockets = sockets;
});
suiteTeardown(function() {
ConnectionManger.Socket = RealSocket;
});
setup(function() {
sockets.length = 0;
subject = new ConnectionManger();
});
suite('initialization', function() {
test('should initialize .connections', function() {
assert.deepEqual(subject.connections, {});
});
});
suite('.remove', function() {
test('should remove connection from list', function() {
subject.open();
assert.ok(subject.connections[0]);
subject.close(0);
assert.notOk(subject.connections[0]);
});
});
suite('.get', function() {
test('should return an open connection', function() {
var open = subject.open();
assert.strictEqual(subject.get(open.id), open.connection);
});
});
suite('.open', function() {
var result, con;
setup(function() {
result = subject.open();
con = subject.connections[0];
});
test('should initialize socket and stream', function() {
assert.ok(sockets[0]);
assert.strictEqual(con.socket, sockets[0]);
assert.strictEqual(sockets[0].port, subject.defaultPort);
assert.instanceOf(con, CommandStream);
});
test('should increment currentId', function() {
assert.strictEqual(subject.currentId, 1);
});
test('should return id and stream', function() {
assert.deepEqual(result, {
id: 0,
connection: subject.connections[0]
});
});
function isRemovedOnEvent(event) {
suite('on socket ' + event, function() {
setup(function() {
assert.ok(subject.connections[0]);
con.socket.emit(event);
});
test('should remove itself from connections', function() {
assert.notOk(subject.connections[0]);
});
});
}
isRemovedOnEvent('close');
isRemovedOnEvent('end');
});
});

View file

@ -0,0 +1,17 @@
'use strict';
var EventEmitter = require('events').EventEmitter;
var WSClient = function() {
this.sendCalls = [];
EventEmitter.call(this);
};
WSClient.prototype = Object.create(EventEmitter.prototype);
WSClient.prototype.onsend = function() {};
WSClient.prototype.send = function() {
this.onsend.apply(this, arguments);
this.sendCalls.push(Array.prototype.slice.call(arguments));
};
module.exports = exports = WSClient;

View file

@ -0,0 +1,121 @@
/* global Marionette, assert */
(function(module, ns) {
'use strict';
function DeviceInteraction(exampleCmds, subject) {
var commands = exampleCmds;
var expectedResponse;
setup(function() {
expectedResponse = null;
});
function driver() {
if (subject().client) {
return subject().client.driver;
}
return subject().driver;
}
return {
commandCallback: function commandCallback(error, value) {
commandCallback.error = error;
commandCallback.value = value;
},
withProtocol: function(version) {
setup(function() {
subject().protocol = version;
});
return this;
},
issues: function issues() {
var args = Array.prototype.slice.call(arguments),
cmd = args.shift(),
result,
commandCallback = this.commandCallback;
setup(function() {
if (!subject()) {
throw new Error('DeviceInteraction must be provided a subject');
}
args.push(commandCallback);
if (!(cmd in subject())) {
throw new Error('client does not have method ' + cmd);
}
result = subject()[cmd].apply(subject(), args);
});
test('should be chainable', function() {
assert.strictEqual(result, subject());
});
return this;
},
shouldSend: function shouldSend(options) {
var check = function(option, value) {
test('should send ' + option, function() {
var sent = driver().sent[0];
if (!(option in sent)) {
throw new Error(
option + ' was never sent as part of command to server');
}
assert.deepEqual(sent[option], value);
});
};
for (var key in options) {
if (options.hasOwnProperty(key)) {
check(key, options[key]);
}
}
return this;
},
serverResponds: function serverResponds(type, options) {
setup(function() {
if (!(type in commands)) {
throw new Error('No such example command: ' + type);
}
expectedResponse = commands[type](options);
driver().respond(expectedResponse);
});
return this;
},
callbackReceives: function callbackReceives(key) {
var commandCallback = this.commandCallback;
if (key) {
test('should receive ' + key + ' in response', function() {
if (typeof expectedResponse[key] == 'undefined') {
throw new Error(key + ' should not be undefined ' +
'for test mocks use a real value');
}
assert.strictEqual(commandCallback.value, expectedResponse[key]);
});
} else {
test('should receive whole body response', function() {
assert.strictEqual(commandCallback.value, expectedResponse);
});
}
return this;
}
};
}
module.exports = DeviceInteraction;
}.apply(
this,
(this.Marionette) ?
[Marionette('support/device-interaction'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,45 @@
/* global Marionette */
(function(module) {
'use strict';
function FakeXhr() {
this.openArgs = null;
this.sendArgs = null;
this.headers = {};
this.responseHeaders = {};
}
FakeXhr.prototype = {
open: function() {
this.openArgs = arguments;
},
getResponseHeader: function(key) {
return this.responseHeaders[key];
},
setRequestHeader: function(key, value) {
this.headers[key] = value;
},
send: function() {
this.sendArgs = arguments;
},
respond: function(data, code) {
this.readyState = 4;
this.responseHeaders['content-type'] = 'application/json';
this.responseText = JSON.stringify(data);
this.status = code || 200;
this.onreadystatechange();
}
};
module.exports = FakeXhr;
}.apply(
this,
(this.Marionette) ?
[Marionette('support/fake-xhr'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,43 @@
/* global Marionette */
(function(module) {
'use strict';
function MockDriver() {
this.sent = [];
this.queue = [];
this.timeout = 0;
}
MockDriver.prototype = {
connectionId: 0,
setScriptTimeout: function(timeout) {
this.timeout = timeout;
},
reset: function() {
this.sent.length = 0;
this.queue.length = 0;
},
send: function(cmd, cb) {
this.sent.push(cmd);
this.queue.push(cb);
},
respond: function(cmd) {
if (this.queue.length) {
(this.queue.shift())(cmd);
}
}
};
module.exports = MockDriver;
}.apply(
this,
(this.Marionette) ?
[Marionette('support/mock-driver'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,45 @@
/* global Marionette */
/**
@namespace
*/
(function(module, ns) {
'use strict';
var Responder = ns.require('responder');
function FakeSocket() {
Responder.call(this);
FakeSocket.sockets.push(this);
this.destroyed = false;
if(arguments.length === 2) {
this.host = arguments[0];
this.port = arguments[1];
}
}
FakeSocket.sockets = [];
FakeSocket.prototype = Object.create(Responder.prototype);
FakeSocket.prototype.connect = function(port, host, cb) {
this.port = port;
this.host = host;
cb && cb();
};
FakeSocket.prototype.destroy = function() {
this.destroyed = true;
};
FakeSocket.prototype.close = FakeSocket.prototype.destroy;
module.exports = exports = FakeSocket;
}.apply(
this,
(this.Marionette) ?
[Marionette('support/socket'), Marionette] :
[module, require('../../lib/marionette/marionette')]
));

View file

@ -0,0 +1,12 @@
{
"name": "Marionette JS Client",
"description": "JavaScript API/Client for Marionette (https://developer.mozilla.org/en-US/docs/Marionette)",
"version": "1.7.1",
"url": "https://github.com/mozilla-b2g/marionette-js-client",
"logo": "none",
"options": {
"paths": "lib/marionette/",
"themedir": "docs/theme",
"linkNatives": true
}
}

View file

@ -0,0 +1,63 @@
'use strict';
var debug = require('debug')('socket-retry-connect'),
net = require('net');
var MAX_TRIES = 10;
var WAIT_BETWEEN_TRIES_MS = 250;
/**
* Tries to connect to a given socket location.
* Time between retires grows in relation to attempts (attempt * RETRY_TIMER).
*
* waitForSocket({ port: 2828, maxTries: 10 }, function(err, socket) {
* });
*
* Note- there is a third argument used to recursion that should
* never be used publicly.
*
* Options:
* - (Number) port: to connect to.
* - (String) host: to connect to.
* - (Number) tries: number of times to attempt the connect.
*
* @param {Object} options for connection.
* @param {Function} callback [err, socket].
*/
function waitForSocket(options, callback, _tries) {
if (!options.port)
throw new Error('.port is a required option');
var maxTries = options.tries || MAX_TRIES;
var host = options.host || 'localhost';
var port = options.port;
debug('attempt to open socket', port, host);
_tries = _tries || 0;
if (_tries >= maxTries)
return callback(new Error('cannot open socket'));
function handleError() {
debug('socket is not ready trying');
// retry connection
setTimeout(
waitForSocket,
// wait at least WAIT_BETWEEN_TRIES_MS or a multiplier
// of the attempts.
(WAIT_BETWEEN_TRIES_MS * _tries) || WAIT_BETWEEN_TRIES_MS,
options,
callback,
++_tries
);
}
var socket = net.connect(port, host, function(one, two) {
debug('connected', port, host);
socket.removeListener('error', handleError);
callback(null, socket);
});
socket.once('error', handleError);
}
module.exports.waitForSocket = waitForSocket;

View file

@ -0,0 +1,11 @@
{
"name": "socket-retry-connect",
"version": "0.0.1",
"author": "James Lal [:lightsofapollo]",
"description": "Attempt to connect to socket multiple times",
"main": "index.js",
"dependencies" : {
"debug": "*"
}
}

View file

@ -0,0 +1,57 @@
'use strict';
suite('socket retry connect', function() {
var PORT = 60012;
var assert = require('assert'),
subject = require('./index').waitForSocket,
net = require('net');
var _server;
function createServer(onConnect) {
onConnect = onConnect || function() {};
// saves reference to last server
return (_server = net.createServer(onConnect).listen(PORT));
}
teardown(function() {
// make sure we actually close the thing
try {
_server.close();
} catch (e) {}
});
function returnsSocket(callback) {
callback = callback || function() {};
return function(err, socket) {
assert.ok(!err, err && err.message);
assert.ok(socket instanceof net.Socket);
callback();
};
}
test('socket is open', function(done) {
var pending = 2;
function next() {
if (--pending === 0) {
done();
}
}
// verify that both the server gets hit and socket callback fires.
createServer(next);
subject({ port: PORT }, returnsSocket(next));
});
test('waits before connection is open', function(done) {
this.timeout('5s');
setTimeout(createServer, 575);
subject({ port: PORT }, returnsSocket(done));
});
test('fails to connect after retires', function(done) {
this.timeout('3s');
subject({ port: PORT, tries: 2 }, function(err) {
assert.ok(err);
assert.ok(err.message.match(/socket/));
done();
});
});
});

View file

@ -0,0 +1,15 @@
{
'variables': {
'gpp': '<!(type g++ | grep "is" > /dev/null && echo "yep" || echo "no")'
},
'targets': [{
'target_name': 'sockit',
'sources': ['src/addon.c',
'src/sockit.cc'],
'conditions': [[
'"<@(gpp)"=="no"',
{ 'sources!': ['src/addon.c',
'src/sockit.cc'] }
]]
}]
}

View file

@ -0,0 +1,25 @@
'use strict';
// Import the add-on.
var sockittome = require('./');
// Create a new sockit object.
var sockit = new sockittome.Sockit();
// Connect to a host and port.
sockit.connect({ host: 'www.google.com', port: 80 });
// Write some data! Takes either a String or a node.js 'Buffer' object.
sockit.write('GET / HTTP/1.0\r\n\r\n');
//sockit.write(new Buffer('GET / HTTP/1.0\r\n\r\n'));
// Read some tasty bytes! Returns a node.js 'Buffer' object, always.
var buffer = sockit.read(1024);
// Observe amazing results.
console.log('Read ', buffer.length, ' bytes');
// And beautiful raw HTTP response.
console.log(buffer.toString());
// Close it up.
sockit.close();

View file

@ -0,0 +1,22 @@
{
"name": "sockit-to-me",
"version": "1.0.2",
"author": "Ghislain 'Aus' Lacroix <aus@mozilla.com>",
"description": "A synchronous socket API for node.js.",
"main": "build/Release/sockit",
"repository": "https://github.com/mozilla-b2g/gaia/tree/master/tests/jsmarionette/client/sockit-to-me",
"license": "MIT",
"engines": {
"node": "^4 || ^5"
},
"devDependencies": {
"node-gyp": "^3.0.1"
},
"scripts": {
"install": "CC=clang CXX=clang++ node-gyp configure build --verbose"
}
}

View file

@ -0,0 +1,15 @@
// Global includes
#include <node.h>
// Local includes
#include "sockit.h"
// So we don't spend half the time typing 'v8::'
using namespace v8;
// Initialize
void InitAll(Handle<Object> aExports) {
Sockit::Init(aExports);
}
NODE_MODULE(sockit, InitAll)

View file

@ -0,0 +1,629 @@
// Global includes
#include <node.h>
#include <node_buffer.h>
// Platform dependent includes.
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netdb.h>
// Local includes
#include "sockit.h"
// So we don't spend half the time typing 'v8::'
using namespace v8;
// Error Messages
#define E_ALREADY_CONNECTING \
"ALREADY CONNECTING! You must call close before calling connect again."
#define E_ALREADY_CONNECTED \
"ALREADY CONNECTED! You must call close before calling connect again."
// Clever constants
static const int MAX_LOOKUP_RETRIES = 3;
static const int POLL_TIMEOUT_MS = 60000;
// Global for debug log enable/disable at runtime.
static bool gDebugLog = false;
Persistent<Function> Sockit::constructor;
static void _debug_log(const char *aMsg) {
if(gDebugLog) {
fprintf(stderr, "[sockit-to-me] %s\n", aMsg);
}
}
Sockit::Sockit() : mSocket(0),
mPollTimeout(POLL_TIMEOUT_MS),
mIsConnecting(false),
mIsConnected(false) {
}
Sockit::~Sockit() {
// Close our socket if it's still open.
if(mSocket != 0) {
// XXXAus: We should probably report error on close.
close(mSocket);
}
}
/*static*/ void
Sockit::Init(Handle<Object> aExports) {
Isolate* isolate = Isolate::GetCurrent();
Local<FunctionTemplate> object = FunctionTemplate::New(isolate, New);
// Set the classname our object will have in JavaScript land.
object->SetClassName(String::NewFromUtf8(isolate, "Sockit"));
// Reserve space for our object instance.
object->InstanceTemplate()->SetInternalFieldCount(1);
// Add the 'Connect' function.
NODE_SET_PROTOTYPE_METHOD(object, "connect", Connect);
// Add the 'Read' function.
NODE_SET_PROTOTYPE_METHOD(object, "read", Read);
// Add the 'Write' function.
NODE_SET_PROTOTYPE_METHOD(object, "write", Write);
// Add the 'Close' function.
NODE_SET_PROTOTYPE_METHOD(object, "close", Close);
// Add the 'SetPollTimeout' function.
NODE_SET_PROTOTYPE_METHOD(object, "setPollTimeout", SetPollTimeout);
// Add the 'SetDebugLog' function.
NODE_SET_PROTOTYPE_METHOD(object, "setDebugLog", SetDebugLog);
// Add the constructor.
constructor.Reset(isolate, object->GetFunction());
aExports->Set(String::NewFromUtf8(isolate, "Sockit"),
object->GetFunction());
}
/*static*/ void
Sockit::New(const FunctionCallbackInfo<Value>& aArgs) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
if (aArgs.IsConstructCall()) {
Sockit* sockit = new Sockit();
sockit->Wrap(aArgs.This());
aArgs.GetReturnValue().Set(aArgs.This());
}
else {
const int argc = 0;
Local<Value> argv[argc] = { };
Local<Function> cons = Local<Function>::New(isolate, constructor);
aArgs.GetReturnValue().Set(cons->NewInstance(argc, argv));
}
}
/*static*/ void
Sockit::Connect(const FunctionCallbackInfo<Value>& aArgs) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
// Always return self.
aArgs.GetReturnValue().Set(aArgs.This());
if(aArgs.Length() < 1) {
isolate->ThrowException(
Exception::TypeError(String::NewFromUtf8(isolate,
"Not enough arguments.")));
return;
}
if(aArgs.Length() > 1) {
isolate->ThrowException(
Exception::TypeError(String::NewFromUtf8(isolate,
"Too many arguments.")));
return;
}
if(!aArgs[0]->IsObject()) {
isolate->ThrowException(
Exception::TypeError(
String::NewFromUtf8(isolate,
"Invalid argument. Must be an Object.")));
return;
}
Local<Object> options = aArgs[0]->ToObject();
Local<String> hostKey = String::NewFromUtf8(isolate, "host");
Local<String> portKey = String::NewFromUtf8(isolate, "port");
if(!options->Has(hostKey) ||
!options->Get(hostKey)->IsString()) {
isolate->ThrowException(
Exception::TypeError(
String::NewFromUtf8(
isolate,
"Object must have 'host' field and it must be a string.")));
return;
}
if(!options->Has(portKey) ||
!options->Get(portKey)->IsNumber()) {
isolate->ThrowException(
Exception::TypeError(
String::NewFromUtf8(
isolate,
"Object must have 'port' field and it must be a number.")));
return;
}
Sockit *sockit = ObjectWrap::Unwrap<Sockit>(aArgs.Holder());
// Ensure we're not already attempting to connect.
if(sockit->mIsConnecting) {
_debug_log(E_ALREADY_CONNECTING);
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate, E_ALREADY_CONNECTING)));
return;
}
// Ensure we're not already connected.
if(sockit->mIsConnected) {
_debug_log(E_ALREADY_CONNECTED);
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate, E_ALREADY_CONNECTED)));
return;
}
// We can consider ourselves in an attempt to connect at this point.
sockit->mIsConnecting = true;
// XXXAus: Move the DNS piece into it's own method.
String::Utf8Value optionsHost(options->Get(hostKey)->ToString());
bool shouldRetry = false;
int retryCount = 0;
struct hostent *hostEntry;
do {
hostEntry = gethostbyname(*optionsHost);
// Getting the host address failed. Let's see why.
if(hostEntry == NULL) {
// Check h_errno, we might retry if the error indicates we should.
switch(h_errno) {
case HOST_NOT_FOUND:
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate,
"Couldn't resolve host.")));
return;
break;
case NO_RECOVERY:
isolate->ThrowException(
Exception::Error(
String::NewFromUtf8(isolate,
"Unexpected failure during host lookup.")));
return;
break;
case NO_DATA:
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate,
"Host exists but has no address.")));
return;
break;
case TRY_AGAIN:
shouldRetry = true;
retryCount++;
break;
}
}
}
while(shouldRetry && retryCount <= MAX_LOOKUP_RETRIES);
if(hostEntry == NULL) {
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate,
"Couldn't resolve host even after retries.")));
return;
}
int socketHandle = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if(socketHandle < 0) {
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate, "Failed to create socket.")));
return;
}
sockit->mSocket = socketHandle;
// Set socket non-blocking prior to connect. We'll poll for connect success.
int flags = fcntl(socketHandle, F_GETFL, 0);
if(flags == -1) {
flags = 0;
}
int result = fcntl(socketHandle, F_SETFL, flags | O_NONBLOCK);
if(result < 0) {
isolate->ThrowException(
Exception::Error(
String::NewFromUtf8(isolate, "Failed to set non blocking to socket.")));
return;
}
// Magic socket incantation.
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
// Always use first address returned, it's good enough for our purposes.
address.sin_addr.s_addr =
inet_addr(inet_ntoa(*(struct in_addr *)hostEntry->h_addr_list[0]));
// Using static cast here to enforce short as required by IPv4 functions.
address.sin_port =
htons(static_cast<short>(options->Get(portKey)->ToNumber()->Uint32Value()));
result = connect(
sockit->mSocket, (sockaddr *) &address, sizeof(struct sockaddr)
);
if(result == -1 && errno != EINPROGRESS) {
// We failed to connect. Close to socket we created.
close(sockit->mSocket);
// In case there's another connect attempt, clean up the socket data.
sockit->mSocket = 0;
sockit->mIsConnecting = false;
// Report failure to connect.
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate, "Failed to connect.")));
return;
}
struct pollfd fds;
fds.fd = sockit->mSocket;
fds.events = POLLWRBAND | POLLWRNORM | POLLNVAL;
fds.revents = 0;
result = poll(&fds, 1, sockit->mPollTimeout);
// If we've failed, we'll clean-up and try and send an exception.
if(result != 1) {
// Close and clean-up.
close(sockit->mSocket);
sockit->mSocket = 0;
sockit->mIsConnecting = false;
sockit->mIsConnected = false;
if(result == 0) {
isolate->ThrowException(
Exception::Error(
String::NewFromUtf8(isolate, "Polling socket connect() timeout!")));
}
else if(result < 0 || (fds.revents & POLLERR)) {
isolate->ThrowException(
Exception::Error(
String::NewFromUtf8(isolate, "Error polling connect() socket!")));
}
else if(!((fds.revents & POLLWRBAND) || (fds.revents & POLLWRNORM)) ) {
isolate->ThrowException(
Exception::Error(
String::NewFromUtf8(isolate, "Unhandled poll event connect() socket!")));
}
return;
}
// Otherwise, we're really connected.
sockit->mIsConnected = true;
// And we're no longer attempting to connect.
sockit->mIsConnecting = false;
}
/*static*/ void
Sockit::Read(const FunctionCallbackInfo<Value>& aArgs) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
// Default to returning undefined
aArgs.GetReturnValue().Set(Undefined(isolate));
if(aArgs.Length() != 1) {
isolate->ThrowException(
Exception::TypeError(
String::NewFromUtf8(
isolate,
"Not enough arguments, read takes the number of bytes to be read from the socket.")));
return;
}
if(!aArgs[0]->IsNumber()) {
isolate->ThrowException(
Exception::TypeError(String::NewFromUtf8(isolate,
"Argument must be a Number.")));
return;
}
Sockit *sockit = ObjectWrap::Unwrap<Sockit>(aArgs.Holder());
// Make sure we're connected to something.
if(sockit->mSocket == 0) {
isolate->ThrowException(
Exception::Error(
String::NewFromUtf8(
isolate,
"Not connected. To read data you must call connect first.")));
return;
}
// Figure out how many bytes the user wants us to read from the socket.
unsigned int bytesToRead = aArgs[0]->ToNumber()->Uint32Value();
// Allocate a shiny buffer.
char *buffer = new char[bytesToRead];
// Track how many bytes we've read and total bytes read.
int totalBytesRead = 0;
int bytesRead = 0;
// Read me some tasty bytes!
struct pollfd fds;
fds.fd = sockit->mSocket;
fds.events = POLLIN;
do {
fds.revents = 0;
int result = poll(&fds, 1, sockit->mPollTimeout);
if(result == 0) {
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate,
"Polling socket recv() timeout!")));
break;
}
else if(result < 0 || (fds.revents & POLLERR)) {
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate,
"Error polling recv() socket!")));
break;
}
else if(!(fds.revents & POLLIN)) {
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate,
"Unhandled poll event recv() socket!")));
break;
}
bytesRead = recv(sockit->mSocket, &buffer[totalBytesRead], bytesToRead, 0);
if(bytesRead < 0) {
if(errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
}
isolate->ThrowException(
Exception::Error(
String::NewFromUtf8(isolate,
"Error while reading from socket!")));
break;
}
if(bytesRead == 0) {
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate,
"Connection reset by peer!")));
break;
}
bytesToRead -= bytesRead;
totalBytesRead += bytesRead;
bytesRead = 0;
}
while(bytesToRead > 0);
if(totalBytesRead) {
// Create a node heap allocated buffer first.
Local<Object> heapBuffer = node::Buffer::New(isolate, totalBytesRead).ToLocalChecked();
// Copy our temporary buffer data into the heap buffer.
memcpy(node::Buffer::Data(heapBuffer), buffer, totalBytesRead);
// Done with our temporary buffer.
delete [] buffer;
// Return those tasty bytes!
aArgs.GetReturnValue().Set(heapBuffer);
return;
}
// Done with our temporary buffer.
delete [] buffer;
// Return NULL!
aArgs.GetReturnValue().Set(Null(isolate));
return;
}
/*static*/ void
Sockit::Write(const FunctionCallbackInfo<Value>& aArgs) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
if(aArgs.Length() < 1) {
isolate->ThrowException(
Exception::TypeError(String::NewFromUtf8(isolate,
"Not enough arguments.")));
return;
}
if(aArgs.Length() > 1) {
isolate->ThrowException(
Exception::TypeError(
String::NewFromUtf8(isolate,
"Too many arguments.")));
return;
}
Sockit *sockit = ObjectWrap::Unwrap<Sockit>(aArgs.Holder());
// Always return self even when we throw.
aArgs.GetReturnValue().Set(aArgs.This());
// Make sure we're connected to something.
if(sockit->mSocket == 0) {
isolate->ThrowException(
Exception::Error(
String::NewFromUtf8(
isolate,
"Not connected. To write data you must call connect first.")));
return;
}
int success = 0;
if(aArgs[0]->IsString()) {
// Convert to utf8 value before sending.
Local<String> dataString = aArgs[0]->ToString();
String::Utf8Value data(dataString);
// Write the data to the socket.
success = sockit->Write(*data, dataString->Utf8Length());
}
else if(aArgs[0]->IsObject() &&
aArgs[0]->ToObject()->GetConstructorName()->Equals(String::NewFromUtf8(isolate, "Buffer"))) {
Local<Object> buffer = aArgs[0]->ToObject();
success =
sockit->Write(node::Buffer::Data(buffer), node::Buffer::Length(buffer));
}
else {
isolate->ThrowException(
Exception::TypeError(
String::NewFromUtf8(isolate,
"Invalid argument, must be a 'String' or 'Buffer' object.")));
return;
}
if(success < 0) {
isolate->ThrowException(
Exception::Error(String::NewFromUtf8(isolate,
"Unable to write data to socket.")));
}
return;
}
int
Sockit::Write(const char *aBuffer, const int aLength) {
int bytesToWrite = aLength;
int totalBytesWritten = 0;
int bytesWritten = 0;
do {
bytesWritten =
send(mSocket,
aBuffer + (totalBytesWritten * sizeof(char)),
bytesToWrite,
0
);
if(bytesWritten < 0) {
return -1;
}
totalBytesWritten += bytesWritten;
bytesToWrite -= bytesWritten;
bytesWritten = 0;
}
while(bytesToWrite > 0);
return totalBytesWritten;
}
/*static*/ void
Sockit::Close(const FunctionCallbackInfo<Value>& aArgs) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
Sockit *sockit = ObjectWrap::Unwrap<Sockit>(aArgs.Holder());
if(sockit->mSocket != 0) {
// If close fails we have bigger problems on our hands than a stale
// socket that isn't closed.
close(sockit->mSocket);
}
sockit->mSocket = 0;
sockit->mIsConnecting = false;
sockit->mIsConnected = false;
aArgs.GetReturnValue().Set(aArgs.This());
return;
}
/*static*/ void
Sockit::SetPollTimeout(const FunctionCallbackInfo<Value>& aArgs) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
if(aArgs.Length() < 1) {
isolate->ThrowException(
Exception::TypeError(String::NewFromUtf8(isolate,
"Not enough arguments.")));
return;
}
if(aArgs.Length() > 1) {
isolate->ThrowException(
Exception::TypeError(String::NewFromUtf8(isolate,
"Too many arguments.")));
return;
}
if(!aArgs[0]->IsNumber()) {
isolate->ThrowException(
Exception::TypeError(String::NewFromUtf8(isolate,
"Argument is not a number.")));
return;
}
Sockit *sockit = ObjectWrap::Unwrap<Sockit>(aArgs.Holder());
sockit->mPollTimeout = aArgs[0]->Int32Value();
aArgs.GetReturnValue().Set(aArgs.This());
return;
}
/*static*/ void
Sockit::SetDebugLog(const FunctionCallbackInfo<Value>& aArgs) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope scope(isolate);
if(aArgs.Length() < 1) {
isolate->ThrowException(
Exception::TypeError(String::NewFromUtf8(isolate,
"Not enough arguments.")));
return;
}
if(aArgs.Length() > 1) {
isolate->ThrowException(
Exception::TypeError(String::NewFromUtf8(isolate,
"Too many arguments.")));
return;
}
if(!aArgs[0]->IsBoolean()) {
isolate->ThrowException(
Exception::TypeError(String::NewFromUtf8(isolate,
"Argument is not a boolean.")));
return;
}
gDebugLog = aArgs[0]->BooleanValue();
if(gDebugLog) {
_debug_log("debug log enabled");
}
aArgs.GetReturnValue().Set(aArgs.This());
return;
}

View file

@ -0,0 +1,39 @@
#ifndef __SOCKIT_H__
#define __SOCKIT_H__
#include <node.h>
#include <node_object_wrap.h>
class Sockit : public node::ObjectWrap {
public:
static void Init(v8::Handle<v8::Object>);
private:
Sockit();
~Sockit();
static v8::Persistent<v8::Function> constructor;
static void New(const v8::FunctionCallbackInfo<v8::Value>&);
static void Connect(const v8::FunctionCallbackInfo<v8::Value>&);
static void Read(const v8::FunctionCallbackInfo<v8::Value>&);
static void Write(const v8::FunctionCallbackInfo<v8::Value>&);
int Write(const char *, const int);
static void Close(const v8::FunctionCallbackInfo<v8::Value>&);
static void SetPollTimeout(const v8::FunctionCallbackInfo<v8::Value>&);
static void SetDebugLog(const v8::FunctionCallbackInfo<v8::Value>&);
int mSocket;
int mPollTimeout;
bool mIsConnecting;
bool mIsConnected;
};
#endif

View file

@ -0,0 +1,380 @@
'use strict';
var assert = require('assert');
var ChildProcess = require('child_process');
// Always use debug binary during testing.
var Sockit = require('..');
suite('sockit-to-me', function() {
// Firing up a process can be slow on Travis CI so we allow a longer
// timeout for our tests.
this.timeout(10000);
var host = '127.0.0.1';
var port = 44321;
var helo = 'HELO\r\n';
var ackn = 'ACK\r\n';
var server = null;
var subject = null;
// global flag set to true if we kill the server
// needed for the close / disconnect
var server_killed = false;
function startServer(callback) {
server_killed = false;
// Start the child process.
server = ChildProcess.fork(__dirname + '/../test_support/server.js');
// Register listener
server.on('message', function(message) {
// Server has actually started listening on the expected host and port.
if(message.reply == 'started') {
// Setup complete.
callback();
}
});
// Ask server to start.
server.send({ command: 'start' });
}
function stopServer(callback) {
// Register listener to shutdown child process once the server has
// successfully closed it's listening socket.
server.on('message', function(message) {
// Server has actually stopped.
if(message.reply == 'stopped') {
// Indicate server was killed.
server_killed = true;
// Disconnect child process.
server.disconnect();
// Teardown complete.
callback();
}
});
// Ask server to stop.
server.send({ command: 'stop' });
}
setup(function(done) {
subject = new Sockit.Sockit();
startServer(done);
});
teardown(function(done) {
// Close connection to server.
subject.close();
if (!server_killed) {
stopServer(done);
} else {
done();
}
});
suite('#connect', function() {
test('successful connections', function(done) {
// Register a listener to ensure that we really did connect to the server
// as we expected to.
server.on('message', function(message) {
if(message.reply == 'connected') {
done();
}
});
// Connect throws on error and is synchronous.
subject.connect({ host: host, port: port });
});
suite('argument shape errors', function() {
test('not enough arguments', function() {
var err;
try {
assert.ok(subject.connect() instanceof Error);
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
test('argument of wrong type', function() {
var err;
try {
assert.ok(subject.connect(23) instanceof Error);
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
test('argument without string "host" attribute', function() {
var err;
try {
subject.connect({ port: 23 });
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
test('argument without number "port" attribute', function() {
var err;
try {
subject.connect({ host: 'host' });
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
});
test('error is thrown when connection cannot be established',
function() {
var err;
// Connect throws on error and is synchronous.
try {
subject.connect({ host: host + 'garbage', port: port });
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
test('connection will timeout when expected when using setPollTimeout',
function() {
subject.setPollTimeout(1000);
var err;
var start = Date.now();
try {
subject.connect({ host: '1.2.3.4', port: 31337 });
} catch(e) {
err = e;
}
assert.ok(Date.now() - start >= 1000);
assert.ok(err instanceof Error);
});
test('calling connect twice will fail', function() {
var err;
try {
subject.connect({ host: host, port: port });
subject.connect({ host: host, port: port });
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
assert.ok(err.message.indexOf('ALREADY CONNECTED') != -1);
});
});
suite('#read', function() {
test('correct data is returned', function(done) {
// Register a listener to ensure that the server really is ready to write
// data when we ask it to.
server.on('message', function(message) {
// Connected, ask server to send data.
if(message.reply == 'connected') {
// Ask server to send 'helo'.
server.send({ command: 'send', data: helo });
// Read the response.
var response = subject.read(helo.length);
// Ensure we got the response we expected.
assert.equal(helo, response.toString());
done();
}
});
// Connect to server.
subject.connect({ host: host, port: port });
});
test('error is thrown when not connected', function() {
var err;
try {
subject.read(1);
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
test('error is thrown when a number is not specified', function() {
var err;
subject.connect({ host: host, port: port });
try {
subject.read();
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
test('1s timeout if no data', function(done) {
subject.setPollTimeout(1000);
// Register a listener to ensure that the server really is ready
server.on('message', function(message) {
// Connected, ask server to send NOTHING.
if(message.reply == 'connected') {
var err;
try {
// Read the response.
subject.read(helo.length);
}
catch(e) {
err = e;
}
assert.ok(err instanceof Error);
done();
}
});
// Connect to server.
subject.connect({ host: host, port: port });
});
test('socket closing', function(done) {
subject.setPollTimeout(1000);
// Register a listener to ensure that the server really is ready
server.on('message', function(message) {
// Connected, ask server to send NOTHING.
if(message.reply == 'connected') {
var err;
server.kill('SIGKILL');
server_killed = true;
try {
// Read the response.
subject.read(helo.length);
}
catch(e) {
err = e;
}
assert.ok(err instanceof Error);
done();
}
});
// Connect to server.
subject.connect({ host: host, port: port });
});
});
suite('#write', function() {
test('specified data is written', function(done) {
// Register a litener to ensure that the server is really ready to read
// data when we ask it to and to verify that it received what we sent.
server.on('message', function(message) {
if(message.reply == 'connected') {
// Tell the server to expect data from the client.
server.send({ command: 'recv', data: ackn });
// Send data to server.
subject.write(ackn);
}
else if(message.reply == 'expected') {
done();
}
});
// Connect to server.
subject.connect({ host: host, port: port });
});
test('error is thrown when not connected', function() {
var err;
try {
subject.write('data');
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
test('error is thrown when no argument is specified', function() {
var err;
subject.connect({ host: host, port: port });
try {
subject.write();
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
test('error is thrown when invalid argument is specified', function() {
var err;
subject.connect({ host: host, port: port });
try {
subject.write({});
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
});
suite('#setDebugLog', function() {
test('succeeds', function() {
subject.setDebugLog(true);
});
test('error is thrown with invalid argument count', function() {
var err;
try {
subject.setDebugLog();
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
test('error is thrown with bad argument type', function() {
var err;
try {
subject.setDebugLog('a string is not the right type');
} catch(e) {
err = e;
}
assert.ok(err instanceof Error);
});
});
});

View file

@ -0,0 +1,65 @@
'use strict';
var net = require('net');
var server = {
_port: 44321,
_host: '127.0.0.1',
_server: null,
_client: null,
_accumulator: null,
start: function() {
this._server = net.createServer(this.connection.bind(this));
this._server.listen(this._port, this._host, function() {
process.send({ reply: 'started' });
});
},
connection: function(socket) {
this._client = socket;
this._accumulator = '';
process.send({ reply: 'connected' });
},
send: function(message) {
this._client.write(message);
},
recv: function(expected) {
this._client.setEncoding('utf8');
this._client.on('data', function(buffer) {
this._accumulator += buffer;
if(this._accumulator == expected) {
process.send({ reply: 'expected' });
}
}.bind(this));
},
stop: function() {
this._server.close(function() {
process.send({ reply: 'stopped' });
});
}
};
process.on('message', function(message) {
switch(message.command) {
case 'start':
server.start();
break;
case 'send':
server.send(message.data);
break;
case 'recv':
server.recv(message.data);
break;
case 'stop':
server.stop();
break;
}
});

Some files were not shown because too many files have changed in this diff Show more