1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/telephony/test/marionette/head.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1202 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + * http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +let Promise = SpecialPowers.Cu.import("resource://gre/modules/Promise.jsm").Promise; 1.8 +let telephony; 1.9 +let conference; 1.10 + 1.11 +/** 1.12 + * Emulator helper. 1.13 + */ 1.14 +let emulator = (function() { 1.15 + let pendingCmdCount = 0; 1.16 + let originalRunEmulatorCmd = runEmulatorCmd; 1.17 + 1.18 + // Overwritten it so people could not call this function directly. 1.19 + runEmulatorCmd = function() { 1.20 + throw "Use emulator.run(cmd, callback) instead of runEmulatorCmd"; 1.21 + }; 1.22 + 1.23 + function run(cmd, callback) { 1.24 + pendingCmdCount++; 1.25 + originalRunEmulatorCmd(cmd, function(result) { 1.26 + pendingCmdCount--; 1.27 + if (callback && typeof callback === "function") { 1.28 + callback(result); 1.29 + } 1.30 + }); 1.31 + } 1.32 + 1.33 + /** 1.34 + * @return Promise 1.35 + */ 1.36 + function waitFinish() { 1.37 + let deferred = Promise.defer(); 1.38 + 1.39 + waitFor(function() { 1.40 + deferred.resolve(); 1.41 + }, function() { 1.42 + return pendingCmdCount === 0; 1.43 + }); 1.44 + 1.45 + return deferred.promise; 1.46 + } 1.47 + 1.48 + return { 1.49 + run: run, 1.50 + waitFinish: waitFinish 1.51 + }; 1.52 +}()); 1.53 + 1.54 +// Delay 1s before each telephony.dial() 1.55 +// The workaround here should be removed after bug 1005816. 1.56 + 1.57 +let originalDial; 1.58 + 1.59 +function delayTelephonyDial() { 1.60 + originalDial = telephony.dial; 1.61 + telephony.dial = function(number, serviceId) { 1.62 + let deferred = Promise.defer(); 1.63 + 1.64 + let startTime = Date.now(); 1.65 + waitFor(function() { 1.66 + originalDial.call(telephony, number, serviceId).then(call => { 1.67 + deferred.resolve(call); 1.68 + }, cause => { 1.69 + deferred.reject(cause); 1.70 + }); 1.71 + }, function() { 1.72 + duration = Date.now() - startTime; 1.73 + return (duration >= 1000); 1.74 + }); 1.75 + 1.76 + return deferred.promise; 1.77 + }; 1.78 +} 1.79 + 1.80 +function restoreTelephonyDial() { 1.81 + telephony.dial = originalDial; 1.82 +} 1.83 + 1.84 +/** 1.85 + * Telephony related helper functions. 1.86 + */ 1.87 +(function() { 1.88 + /** 1.89 + * @return Promise 1.90 + */ 1.91 + function clearCalls() { 1.92 + let deferred = Promise.defer(); 1.93 + 1.94 + log("Clear existing calls."); 1.95 + emulator.run("gsm clear", function(result) { 1.96 + if (result[0] == "OK") { 1.97 + waitFor(function() { 1.98 + deferred.resolve(); 1.99 + }, function() { 1.100 + return telephony.calls.length === 0; 1.101 + }); 1.102 + } else { 1.103 + log("Failed to clear existing calls."); 1.104 + deferred.reject(); 1.105 + } 1.106 + }); 1.107 + 1.108 + return deferred.promise; 1.109 + } 1.110 + 1.111 + /** 1.112 + * Provide a string with format of the emulator call list result. 1.113 + * 1.114 + * @param prefix 1.115 + * Possible values are "outbound" and "inbound". 1.116 + * @param number 1.117 + * Call number. 1.118 + * @return A string with format of the emulator call list result. 1.119 + */ 1.120 + function callStrPool(prefix, number) { 1.121 + let padding = " : "; 1.122 + let numberInfo = prefix + number + padding.substr(number.length); 1.123 + 1.124 + let info = {}; 1.125 + let states = ['ringing', 'incoming', 'active', 'held']; 1.126 + for (let state of states) { 1.127 + info[state] = numberInfo + state; 1.128 + } 1.129 + 1.130 + return info; 1.131 + } 1.132 + 1.133 + /** 1.134 + * Provide a corresponding string of an outgoing call. The string is with 1.135 + * format of the emulator call list result. 1.136 + * 1.137 + * @param number 1.138 + * Number of an outgoing call. 1.139 + * @return A string with format of the emulator call list result. 1.140 + */ 1.141 + function outCallStrPool(number) { 1.142 + return callStrPool("outbound to ", number); 1.143 + } 1.144 + 1.145 + /** 1.146 + * Provide a corresponding string of an incoming call. The string is with 1.147 + * format of the emulator call list result. 1.148 + * 1.149 + * @param number 1.150 + * Number of an incoming call. 1.151 + * @return A string with format of the emulator call list result. 1.152 + */ 1.153 + function inCallStrPool(number) { 1.154 + return callStrPool("inbound from ", number); 1.155 + } 1.156 + 1.157 + /** 1.158 + * Check utility functions. 1.159 + */ 1.160 + 1.161 + function checkInitialState() { 1.162 + log("Verify initial state."); 1.163 + ok(telephony.calls, 'telephony.call'); 1.164 + checkTelephonyActiveAndCalls(null, []); 1.165 + ok(conference.calls, 'conference.calls'); 1.166 + checkConferenceStateAndCalls('', []); 1.167 + } 1.168 + 1.169 + /** 1.170 + * Convenient helper to compare a TelephonyCall and a received call event. 1.171 + */ 1.172 + function checkEventCallState(event, call, state) { 1.173 + is(call, event.call, "event.call"); 1.174 + is(call.state, state, "call state"); 1.175 + } 1.176 + 1.177 + /** 1.178 + * Convenient helper to check mozTelephony.active and mozTelephony.calls. 1.179 + */ 1.180 + function checkTelephonyActiveAndCalls(active, calls) { 1.181 + is(telephony.active, active, "telephony.active"); 1.182 + is(telephony.calls.length, calls.length, "telephony.calls"); 1.183 + for (let i = 0; i < calls.length; ++i) { 1.184 + is(telephony.calls[i], calls[i]); 1.185 + } 1.186 + } 1.187 + 1.188 + /** 1.189 + * Convenient helper to check mozTelephony.conferenceGroup.state and 1.190 + * .conferenceGroup.calls. 1.191 + */ 1.192 + function checkConferenceStateAndCalls(state, calls) { 1.193 + is(conference.state, state, "conference.state"); 1.194 + is(conference.calls.length, calls.length, "conference.calls"); 1.195 + for (let i = 0; i < calls.length; i++) { 1.196 + is(conference.calls[i], calls[i]); 1.197 + } 1.198 + } 1.199 + 1.200 + /** 1.201 + * Convenient helper to handle *.oncallschanged event. 1.202 + * 1.203 + * @param container 1.204 + * Representation of "mozTelephony" or "mozTelephony.conferenceGroup." 1.205 + * @param containerName 1.206 + * Name of container. Could be an arbitrary string, used for debug 1.207 + * messages only. 1.208 + * @param expectedCalls 1.209 + * An array of calls. 1.210 + * @param callback 1.211 + * A callback function. 1.212 + */ 1.213 + function check_oncallschanged(container, containerName, expectedCalls, 1.214 + callback) { 1.215 + container.oncallschanged = function(event) { 1.216 + log("Received 'callschanged' event for the " + containerName); 1.217 + if (event.call) { 1.218 + let index = expectedCalls.indexOf(event.call); 1.219 + ok(index != -1); 1.220 + expectedCalls.splice(index, 1); 1.221 + 1.222 + if (expectedCalls.length === 0) { 1.223 + container.oncallschanged = null; 1.224 + callback(); 1.225 + } 1.226 + } 1.227 + }; 1.228 + } 1.229 + 1.230 + /** 1.231 + * Convenient helper to handle *.ongroupchange event. 1.232 + * 1.233 + * @param call 1.234 + * A TelephonyCall object. 1.235 + * @param callName 1.236 + * Name of a call. Could be an arbitrary string, used for debug messages 1.237 + * only. 1.238 + * @param group 1.239 + * Representation of mozTelephony.conferenceGroup. 1.240 + * @param callback 1.241 + * A callback function. 1.242 + */ 1.243 + function check_ongroupchange(call, callName, group, callback) { 1.244 + call.ongroupchange = function(event) { 1.245 + log("Received 'groupchange' event for the " + callName); 1.246 + call.ongroupchange = null; 1.247 + 1.248 + is(call.group, group); 1.249 + callback(); 1.250 + }; 1.251 + } 1.252 + 1.253 + /** 1.254 + * Convenient helper to handle *.onstatechange event. 1.255 + * 1.256 + * @param container 1.257 + * Representation of a TelephonyCall or mozTelephony.conferenceGroup. 1.258 + * @param containerName 1.259 + * Name of container. Could be an arbitrary string, used for debug messages 1.260 + * only. 1.261 + * @param state 1.262 + * A string. 1.263 + * @param callback 1.264 + * A callback function. 1.265 + */ 1.266 + function check_onstatechange(container, containerName, state, callback) { 1.267 + container.onstatechange = function(event) { 1.268 + log("Received 'statechange' event for the " + containerName); 1.269 + container.onstatechange = null; 1.270 + 1.271 + is(container.state, state); 1.272 + callback(); 1.273 + }; 1.274 + } 1.275 + 1.276 + /** 1.277 + * Convenient helper to check the sequence of call state and event handlers. 1.278 + * 1.279 + * @param state 1.280 + * A string of the expected call state. 1.281 + * @param previousEvent 1.282 + * A string of the event that should come before the expected state. 1.283 + */ 1.284 + function StateEventChecker(state, previousEvent) { 1.285 + let event = 'on' + state; 1.286 + 1.287 + return function(call, callName, callback) { 1.288 + call[event] = function() { 1.289 + log("Received '" + state + "' event for the " + callName); 1.290 + call[event] = null; 1.291 + 1.292 + if (previousEvent) { 1.293 + // We always clear the event handler when the event is received. 1.294 + // Therefore, if the corresponding handler is not existed, the expected 1.295 + // previous event has been already received. 1.296 + ok(!call[previousEvent]); 1.297 + } 1.298 + is(call.state, state); 1.299 + callback(); 1.300 + }; 1.301 + }; 1.302 + } 1.303 + 1.304 + /** 1.305 + * Convenient helper to check the call list existing in the emulator. 1.306 + * 1.307 + * @param expectedCallList 1.308 + * An array of call info with the format of "callStrPool()[state]". 1.309 + * @return A deferred promise. 1.310 + */ 1.311 + function checkEmulatorCallList(expectedCallList) { 1.312 + let deferred = Promise.defer(); 1.313 + 1.314 + emulator.run("gsm list", function(result) { 1.315 + log("Call list is now: " + result); 1.316 + for (let i = 0; i < expectedCallList.length; ++i) { 1.317 + is(result[i], expectedCallList[i], "emulator calllist"); 1.318 + } 1.319 + is(result[expectedCallList.length], "OK", "emulator calllist"); 1.320 + deferred.resolve(); 1.321 + }); 1.322 + 1.323 + return deferred.promise; 1.324 + } 1.325 + 1.326 + /** 1.327 + * Super convenient helper to check calls and state of mozTelephony and 1.328 + * mozTelephony.conferenceGroup. 1.329 + * 1.330 + * @param active 1.331 + * A TelephonyCall object. Should be the expected active call. 1.332 + * @param calls 1.333 + * An array of TelephonyCall objects. Should be the expected list of 1.334 + * mozTelephony.calls. 1.335 + * @param conferenceState 1.336 + * A string. Should be the expected conference state. 1.337 + * @param conferenceCalls 1.338 + * An array of TelephonyCall objects. Should be the expected list of 1.339 + * mozTelephony.conferenceGroup.calls. 1.340 + */ 1.341 + function checkState(active, calls, conferenceState, conferenceCalls) { 1.342 + checkTelephonyActiveAndCalls(active, calls); 1.343 + checkConferenceStateAndCalls(conferenceState, conferenceCalls); 1.344 + } 1.345 + 1.346 + /** 1.347 + * Super convenient helper to check calls and state of mozTelephony and 1.348 + * mozTelephony.conferenceGroup as well as the calls existing in the emulator. 1.349 + * 1.350 + * @param active 1.351 + * A TelephonyCall object. Should be the expected active call. 1.352 + * @param calls 1.353 + * An array of TelephonyCall objects. Should be the expected list of 1.354 + * mozTelephony.calls. 1.355 + * @param conferenceState 1.356 + * A string. Should be the expected conference state. 1.357 + * @param conferenceCalls 1.358 + * An array of TelephonyCall objects. Should be the expected list of 1.359 + * mozTelephony.conferenceGroup.calls. 1.360 + * @param callList 1.361 + * An array of call info with the format of "callStrPool()[state]". 1.362 + * @return A deferred promise. 1.363 + */ 1.364 + function checkAll(active, calls, conferenceState, conferenceCalls, callList) { 1.365 + checkState(active, calls, conferenceState, conferenceCalls); 1.366 + return checkEmulatorCallList(callList); 1.367 + } 1.368 + 1.369 + /** 1.370 + * Request utility functions. 1.371 + */ 1.372 + 1.373 + /** 1.374 + * Make sure there's no pending event before we jump to the next action. 1.375 + * 1.376 + * @param received 1.377 + * A string of the received event. 1.378 + * @param pending 1.379 + * An array of the pending events. 1.380 + * @param nextAction 1.381 + * A callback function that is called when there's no pending event. 1.382 + */ 1.383 + function receivedPending(received, pending, nextAction) { 1.384 + let index = pending.indexOf(received); 1.385 + if (index != -1) { 1.386 + pending.splice(index, 1); 1.387 + } 1.388 + if (pending.length === 0) { 1.389 + nextAction(); 1.390 + } 1.391 + } 1.392 + 1.393 + /** 1.394 + * Make an outgoing call. 1.395 + * 1.396 + * @param number 1.397 + * A string. 1.398 + * @param serviceId [optional] 1.399 + * Identification of a service. 0 is set as default. 1.400 + * @return A deferred promise. 1.401 + */ 1.402 + function dial(number, serviceId) { 1.403 + serviceId = typeof serviceId !== "undefined" ? serviceId : 0; 1.404 + log("Make an outgoing call: " + number + ", serviceId: " + serviceId); 1.405 + 1.406 + let deferred = Promise.defer(); 1.407 + 1.408 + telephony.dial(number, serviceId).then(call => { 1.409 + ok(call); 1.410 + is(call.number, number); 1.411 + is(call.state, "dialing"); 1.412 + is(call.serviceId, serviceId); 1.413 + 1.414 + call.onalerting = function onalerting(event) { 1.415 + call.onalerting = null; 1.416 + log("Received 'onalerting' call event."); 1.417 + checkEventCallState(event, call, "alerting"); 1.418 + deferred.resolve(call); 1.419 + }; 1.420 + }, cause => { 1.421 + deferred.reject(cause); 1.422 + }); 1.423 + 1.424 + return deferred.promise; 1.425 + } 1.426 + 1.427 + /** 1.428 + * Answer an incoming call. 1.429 + * 1.430 + * @param call 1.431 + * An incoming TelephonyCall object. 1.432 + * @param conferenceStateChangeCallback [optional] 1.433 + * A callback function which is called if answering an incoming call 1.434 + * triggers conference state change. 1.435 + * @return A deferred promise. 1.436 + */ 1.437 + function answer(call, conferenceStateChangeCallback) { 1.438 + log("Answering the incoming call."); 1.439 + 1.440 + let deferred = Promise.defer(); 1.441 + let done = function() { 1.442 + deferred.resolve(call); 1.443 + }; 1.444 + 1.445 + let pending = ["call.onconnected"]; 1.446 + let receive = function(name) { 1.447 + receivedPending(name, pending, done); 1.448 + }; 1.449 + 1.450 + // When there's already a connected conference call, answering a new incoming 1.451 + // call triggers conference state change. We should wait for 1.452 + // |conference.onstatechange| before checking the state of the conference call. 1.453 + if (conference.state === "connected") { 1.454 + pending.push("conference.onstatechange"); 1.455 + check_onstatechange(conference, "conference", "held", function() { 1.456 + if (typeof conferenceStateChangeCallback === "function") { 1.457 + conferenceStateChangeCallback(); 1.458 + } 1.459 + receive("conference.onstatechange"); 1.460 + }); 1.461 + } 1.462 + 1.463 + call.onconnecting = function onconnectingIn(event) { 1.464 + log("Received 'connecting' call event for incoming call."); 1.465 + call.onconnecting = null; 1.466 + checkEventCallState(event, call, "connecting"); 1.467 + }; 1.468 + 1.469 + call.onconnected = function onconnectedIn(event) { 1.470 + log("Received 'connected' call event for incoming call."); 1.471 + call.onconnected = null; 1.472 + checkEventCallState(event, call, "connected"); 1.473 + ok(!call.onconnecting); 1.474 + receive("call.onconnected"); 1.475 + }; 1.476 + call.answer(); 1.477 + 1.478 + return deferred.promise; 1.479 + } 1.480 + 1.481 + /** 1.482 + * Hold a call. 1.483 + * 1.484 + * @param call 1.485 + * A TelephonyCall object. 1.486 + * @return A deferred promise. 1.487 + */ 1.488 + function hold(call) { 1.489 + log("Putting the call on hold."); 1.490 + 1.491 + let deferred = Promise.defer(); 1.492 + 1.493 + let gotHolding = false; 1.494 + call.onholding = function onholding(event) { 1.495 + log("Received 'holding' call event"); 1.496 + call.onholding = null; 1.497 + checkEventCallState(event, call, "holding"); 1.498 + gotHolding = true; 1.499 + }; 1.500 + 1.501 + call.onheld = function onheld(event) { 1.502 + log("Received 'held' call event"); 1.503 + call.onheld = null; 1.504 + checkEventCallState(event, call, "held"); 1.505 + ok(gotHolding); 1.506 + deferred.resolve(call); 1.507 + }; 1.508 + call.hold(); 1.509 + 1.510 + return deferred.promise; 1.511 + } 1.512 + 1.513 + /** 1.514 + * Simulate an incoming call. 1.515 + * 1.516 + * @param number 1.517 + * A string. 1.518 + * @return A deferred promise. 1.519 + */ 1.520 + function remoteDial(number) { 1.521 + log("Simulating an incoming call."); 1.522 + 1.523 + let deferred = Promise.defer(); 1.524 + 1.525 + telephony.onincoming = function onincoming(event) { 1.526 + log("Received 'incoming' call event."); 1.527 + telephony.onincoming = null; 1.528 + 1.529 + let call = event.call; 1.530 + 1.531 + ok(call); 1.532 + is(call.number, number); 1.533 + is(call.state, "incoming"); 1.534 + 1.535 + deferred.resolve(call); 1.536 + }; 1.537 + emulator.run("gsm call " + number); 1.538 + 1.539 + return deferred.promise; 1.540 + } 1.541 + 1.542 + /** 1.543 + * Remote party answers the call. 1.544 + * 1.545 + * @param call 1.546 + * A TelephonyCall object. 1.547 + * @return A deferred promise. 1.548 + */ 1.549 + function remoteAnswer(call) { 1.550 + log("Remote answering the call."); 1.551 + 1.552 + let deferred = Promise.defer(); 1.553 + 1.554 + call.onconnected = function onconnected(event) { 1.555 + log("Received 'connected' call event."); 1.556 + call.onconnected = null; 1.557 + checkEventCallState(event, call, "connected"); 1.558 + deferred.resolve(call); 1.559 + }; 1.560 + emulator.run("gsm accept " + call.number); 1.561 + 1.562 + return deferred.promise; 1.563 + } 1.564 + 1.565 + /** 1.566 + * Remote party hangs up the call. 1.567 + * 1.568 + * @param call 1.569 + * A TelephonyCall object. 1.570 + * @return A deferred promise. 1.571 + */ 1.572 + function remoteHangUp(call) { 1.573 + log("Remote hanging up the call."); 1.574 + 1.575 + let deferred = Promise.defer(); 1.576 + 1.577 + call.ondisconnected = function ondisconnected(event) { 1.578 + log("Received 'disconnected' call event."); 1.579 + call.ondisconnected = null; 1.580 + checkEventCallState(event, call, "disconnected"); 1.581 + deferred.resolve(call); 1.582 + }; 1.583 + emulator.run("gsm cancel " + call.number); 1.584 + 1.585 + return deferred.promise; 1.586 + } 1.587 + 1.588 + /** 1.589 + * Remote party hangs up all the calls. 1.590 + * 1.591 + * @param calls 1.592 + * An array of TelephonyCall objects. 1.593 + * @return A deferred promise. 1.594 + */ 1.595 + function remoteHangUpCalls(calls) { 1.596 + let promise = Promise.resolve(); 1.597 + 1.598 + for (let call of calls) { 1.599 + promise = promise.then(remoteHangUp.bind(null, call)); 1.600 + } 1.601 + 1.602 + return promise; 1.603 + } 1.604 + 1.605 + /** 1.606 + * Add calls to conference. 1.607 + * 1.608 + * @param callsToAdd 1.609 + * An array of TelephonyCall objects to be added into conference. The 1.610 + * length of the array should be 1 or 2. 1.611 + * @param connectedCallback [optional] 1.612 + * A callback function which is called when conference state becomes 1.613 + * connected. 1.614 + * @return A deferred promise. 1.615 + */ 1.616 + function addCallsToConference(callsToAdd, connectedCallback) { 1.617 + log("Add " + callsToAdd.length + " calls into conference."); 1.618 + 1.619 + let deferred = Promise.defer(); 1.620 + let done = function() { 1.621 + deferred.resolve(); 1.622 + }; 1.623 + 1.624 + let pending = ["conference.oncallschanged", "conference.onconnected"]; 1.625 + let receive = function(name) { 1.626 + receivedPending(name, pending, done); 1.627 + }; 1.628 + 1.629 + let check_onconnected = StateEventChecker('connected', 'onresuming'); 1.630 + 1.631 + for (let call of callsToAdd) { 1.632 + let callName = "callToAdd (" + call.number + ')'; 1.633 + 1.634 + let ongroupchange = callName + ".ongroupchange"; 1.635 + pending.push(ongroupchange); 1.636 + check_ongroupchange(call, callName, conference, 1.637 + receive.bind(null, ongroupchange)); 1.638 + 1.639 + let onstatechange = callName + ".onstatechange"; 1.640 + pending.push(onstatechange); 1.641 + check_onstatechange(call, callName, 'connected', 1.642 + receive.bind(null, onstatechange)); 1.643 + } 1.644 + 1.645 + check_oncallschanged(conference, 'conference', callsToAdd, 1.646 + receive.bind(null, "conference.oncallschanged")); 1.647 + 1.648 + check_onconnected(conference, "conference", function() { 1.649 + ok(!conference.oncallschanged); 1.650 + if (typeof connectedCallback === 'function') { 1.651 + connectedCallback(); 1.652 + } 1.653 + receive("conference.onconnected"); 1.654 + }); 1.655 + 1.656 + // Cannot use apply() through webidl, so just separate the cases to handle. 1.657 + if (callsToAdd.length == 2) { 1.658 + conference.add(callsToAdd[0], callsToAdd[1]); 1.659 + } else { 1.660 + conference.add(callsToAdd[0]); 1.661 + } 1.662 + 1.663 + return deferred.promise; 1.664 + } 1.665 + 1.666 + /** 1.667 + * Hold the conference. 1.668 + * 1.669 + * @param calls 1.670 + * An array of TelephonyCall objects existing in conference. 1.671 + * @param heldCallback [optional] 1.672 + * A callback function which is called when conference state becomes 1.673 + * held. 1.674 + * @return A deferred promise. 1.675 + */ 1.676 + function holdConference(calls, heldCallback) { 1.677 + log("Holding the conference call."); 1.678 + 1.679 + let deferred = Promise.defer(); 1.680 + let done = function() { 1.681 + deferred.resolve(); 1.682 + }; 1.683 + 1.684 + let pending = ["conference.onholding", "conference.onheld"]; 1.685 + let receive = function(name) { 1.686 + receivedPending(name, pending, done); 1.687 + }; 1.688 + 1.689 + let check_onholding = StateEventChecker('holding', null); 1.690 + let check_onheld = StateEventChecker('held', 'onholding'); 1.691 + 1.692 + for (let call of calls) { 1.693 + let callName = "call (" + call.number + ')'; 1.694 + 1.695 + let onholding = callName + ".onholding"; 1.696 + pending.push(onholding); 1.697 + check_onholding(call, callName, receive.bind(null, onholding)); 1.698 + 1.699 + let onheld = callName + ".onheld"; 1.700 + pending.push(onheld); 1.701 + check_onheld(call, callName, receive.bind(null, onheld)); 1.702 + } 1.703 + 1.704 + check_onholding(conference, "conference", 1.705 + receive.bind(null, "conference.onholding")); 1.706 + 1.707 + check_onheld(conference, "conference", function() { 1.708 + if (typeof heldCallback === 'function') { 1.709 + heldCallback(); 1.710 + } 1.711 + receive("conference.onheld"); 1.712 + }); 1.713 + 1.714 + conference.hold(); 1.715 + 1.716 + return deferred.promise; 1.717 + } 1.718 + 1.719 + /** 1.720 + * Resume the conference. 1.721 + * 1.722 + * @param calls 1.723 + * An array of TelephonyCall objects existing in conference. 1.724 + * @param connectedCallback [optional] 1.725 + * A callback function which is called when conference state becomes 1.726 + * connected. 1.727 + * @return A deferred promise. 1.728 + */ 1.729 + function resumeConference(calls, connectedCallback) { 1.730 + log("Resuming the held conference call."); 1.731 + 1.732 + let deferred = Promise.defer(); 1.733 + let done = function() { 1.734 + deferred.resolve(); 1.735 + }; 1.736 + 1.737 + let pending = ["conference.onresuming", "conference.onconnected"]; 1.738 + let receive = function(name) { 1.739 + receivedPending(name, pending, done); 1.740 + }; 1.741 + 1.742 + let check_onresuming = StateEventChecker('resuming', null); 1.743 + let check_onconnected = StateEventChecker('connected', 'onresuming'); 1.744 + 1.745 + for (let call of calls) { 1.746 + let callName = "call (" + call.number + ')'; 1.747 + 1.748 + let onresuming = callName + ".onresuming"; 1.749 + pending.push(onresuming); 1.750 + check_onresuming(call, callName, receive.bind(null, onresuming)); 1.751 + 1.752 + let onconnected = callName + ".onconnected"; 1.753 + pending.push(onconnected); 1.754 + check_onconnected(call, callName, receive.bind(null, onconnected)); 1.755 + } 1.756 + 1.757 + check_onresuming(conference, "conference", 1.758 + receive.bind(null, "conference.onresuming")); 1.759 + 1.760 + check_onconnected(conference, "conference", function() { 1.761 + if (typeof connectedCallback === 'function') { 1.762 + connectedCallback(); 1.763 + } 1.764 + receive("conference.onconnected"); 1.765 + }); 1.766 + 1.767 + conference.resume(); 1.768 + 1.769 + return deferred.promise; 1.770 + } 1.771 + 1.772 + /** 1.773 + * Remove a call out of conference. 1.774 + * 1.775 + * @param callToRemove 1.776 + * A TelephonyCall object existing in conference. 1.777 + * @param autoRemovedCalls 1.778 + * An array of TelephonyCall objects which is going to be automatically 1.779 + * removed. The length of the array should be 0 or 1. 1.780 + * @param remainedCalls 1.781 + * An array of TelephonyCall objects which remain in conference. 1.782 + * @param stateChangeCallback [optional] 1.783 + * A callback function which is called when conference state changes. 1.784 + * @return A deferred promise. 1.785 + */ 1.786 + function removeCallInConference(callToRemove, autoRemovedCalls, remainedCalls, 1.787 + statechangeCallback) { 1.788 + log("Removing a participant from the conference call."); 1.789 + 1.790 + is(conference.state, 'connected'); 1.791 + 1.792 + let deferred = Promise.defer(); 1.793 + let done = function() { 1.794 + deferred.resolve(); 1.795 + }; 1.796 + 1.797 + let pending = ["callToRemove.ongroupchange", "telephony.oncallschanged", 1.798 + "conference.oncallschanged", "conference.onstatechange"]; 1.799 + let receive = function(name) { 1.800 + receivedPending(name, pending, done); 1.801 + }; 1.802 + 1.803 + // Remained call in conference will be held. 1.804 + for (let call of remainedCalls) { 1.805 + let callName = "remainedCall (" + call.number + ')'; 1.806 + 1.807 + let onstatechange = callName + ".onstatechange"; 1.808 + pending.push(onstatechange); 1.809 + check_onstatechange(call, callName, 'held', 1.810 + receive.bind(null, onstatechange)); 1.811 + } 1.812 + 1.813 + // When a call is removed from conference with 2 calls, another one will be 1.814 + // automatically removed from group and be put on hold. 1.815 + for (let call of autoRemovedCalls) { 1.816 + let callName = "autoRemovedCall (" + call.number + ')'; 1.817 + 1.818 + let ongroupchange = callName + ".ongroupchange"; 1.819 + pending.push(ongroupchange); 1.820 + check_ongroupchange(call, callName, null, 1.821 + receive.bind(null, ongroupchange)); 1.822 + 1.823 + let onstatechange = callName + ".onstatechange"; 1.824 + pending.push(onstatechange); 1.825 + check_onstatechange(call, callName, 'held', 1.826 + receive.bind(null, onstatechange)); 1.827 + } 1.828 + 1.829 + check_ongroupchange(callToRemove, "callToRemove", null, function() { 1.830 + is(callToRemove.state, 'connected'); 1.831 + receive("callToRemove.ongroupchange"); 1.832 + }); 1.833 + 1.834 + check_oncallschanged(telephony, 'telephony', 1.835 + autoRemovedCalls.concat(callToRemove), 1.836 + receive.bind(null, "telephony.oncallschanged")); 1.837 + 1.838 + check_oncallschanged(conference, 'conference', 1.839 + autoRemovedCalls.concat(callToRemove), function() { 1.840 + is(conference.calls.length, remainedCalls.length); 1.841 + receive("conference.oncallschanged"); 1.842 + }); 1.843 + 1.844 + check_onstatechange(conference, 'conference', 1.845 + (remainedCalls.length ? 'held' : ''), function() { 1.846 + ok(!conference.oncallschanged); 1.847 + if (typeof statechangeCallback === 'function') { 1.848 + statechangeCallback(); 1.849 + } 1.850 + receive("conference.onstatechange"); 1.851 + }); 1.852 + 1.853 + conference.remove(callToRemove); 1.854 + 1.855 + return deferred.promise; 1.856 + } 1.857 + 1.858 + /** 1.859 + * Hangup a call in conference. 1.860 + * 1.861 + * @param callToHangUp 1.862 + * A TelephonyCall object existing in conference. 1.863 + * @param autoRemovedCalls 1.864 + * An array of TelephonyCall objects which is going to be automatically 1.865 + * removed. The length of the array should be 0 or 1. 1.866 + * @param remainedCalls 1.867 + * An array of TelephonyCall objects which remain in conference. 1.868 + * @param stateChangeCallback [optional] 1.869 + * A callback function which is called when conference state changes. 1.870 + * @return A deferred promise. 1.871 + */ 1.872 + function hangUpCallInConference(callToHangUp, autoRemovedCalls, remainedCalls, 1.873 + statechangeCallback) { 1.874 + log("Release one call in conference."); 1.875 + 1.876 + let deferred = Promise.defer(); 1.877 + let done = function() { 1.878 + deferred.resolve(); 1.879 + }; 1.880 + 1.881 + let pending = ["conference.oncallschanged", "remoteHangUp"]; 1.882 + let receive = function(name) { 1.883 + receivedPending(name, pending, done); 1.884 + }; 1.885 + 1.886 + // When a call is hang up from conference with 2 calls, another one will be 1.887 + // automatically removed from group. 1.888 + for (let call of autoRemovedCalls) { 1.889 + let callName = "autoRemovedCall (" + call.number + ')'; 1.890 + 1.891 + let ongroupchange = callName + ".ongroupchange"; 1.892 + pending.push(ongroupchange); 1.893 + check_ongroupchange(call, callName, null, 1.894 + receive.bind(null, ongroupchange)); 1.895 + } 1.896 + 1.897 + if (autoRemovedCalls.length) { 1.898 + pending.push("telephony.oncallschanged"); 1.899 + check_oncallschanged(telephony, 'telephony', 1.900 + autoRemovedCalls, 1.901 + receive.bind(null, "telephony.oncallschanged")); 1.902 + } 1.903 + 1.904 + check_oncallschanged(conference, 'conference', 1.905 + autoRemovedCalls.concat(callToHangUp), function() { 1.906 + is(conference.calls.length, remainedCalls.length); 1.907 + receive("conference.oncallschanged"); 1.908 + }); 1.909 + 1.910 + if (remainedCalls.length === 0) { 1.911 + pending.push("conference.onstatechange"); 1.912 + check_onstatechange(conference, 'conference', '', function() { 1.913 + ok(!conference.oncallschanged); 1.914 + if (typeof statechangeCallback === 'function') { 1.915 + statechangeCallback(); 1.916 + } 1.917 + receive("conference.onstatechange"); 1.918 + }); 1.919 + } 1.920 + 1.921 + remoteHangUp(callToHangUp) 1.922 + .then(receive.bind(null, "remoteHangUp")); 1.923 + 1.924 + return deferred.promise; 1.925 + } 1.926 + 1.927 + /** 1.928 + * Setup a conference with an outgoing call and an incoming call. 1.929 + * 1.930 + * @param outNumber 1.931 + * Number of an outgoing call. 1.932 + * @param inNumber 1.933 + * Number of an incoming call. 1.934 + * @return Promise<[outCall, inCall]> 1.935 + */ 1.936 + function setupConferenceTwoCalls(outNumber, inNumber) { 1.937 + log('Create conference with two calls.'); 1.938 + 1.939 + let outCall; 1.940 + let inCall; 1.941 + let outInfo = outCallStrPool(outNumber); 1.942 + let inInfo = inCallStrPool(inNumber); 1.943 + 1.944 + return Promise.resolve() 1.945 + .then(checkInitialState) 1.946 + .then(() => dial(outNumber)) 1.947 + .then(call => { outCall = call; }) 1.948 + .then(() => checkAll(outCall, [outCall], '', [], [outInfo.ringing])) 1.949 + .then(() => remoteAnswer(outCall)) 1.950 + .then(() => checkAll(outCall, [outCall], '', [], [outInfo.active])) 1.951 + .then(() => remoteDial(inNumber)) 1.952 + .then(call => { inCall = call; }) 1.953 + .then(() => checkAll(outCall, [outCall, inCall], '', [], 1.954 + [outInfo.active, inInfo.incoming])) 1.955 + .then(() => answer(inCall)) 1.956 + .then(() => checkAll(inCall, [outCall, inCall], '', [], 1.957 + [outInfo.held, inInfo.active])) 1.958 + .then(() => addCallsToConference([outCall, inCall], function() { 1.959 + checkState(conference, [], 'connected', [outCall, inCall]); 1.960 + })) 1.961 + .then(() => checkAll(conference, [], 'connected', [outCall, inCall], 1.962 + [outInfo.active, inInfo.active])) 1.963 + .then(() => { 1.964 + return [outCall, inCall]; 1.965 + }); 1.966 + } 1.967 + 1.968 + /** 1.969 + * Setup a conference with an outgoing call and two incoming calls. 1.970 + * 1.971 + * @param outNumber 1.972 + * Number of an outgoing call. 1.973 + * @param inNumber 1.974 + * Number of an incoming call. 1.975 + * @param inNumber2 1.976 + * Number of an incoming call. 1.977 + * @return Promise<[outCall, inCall, inCall2]> 1.978 + */ 1.979 + function setupConferenceThreeCalls(outNumber, inNumber, inNumber2) { 1.980 + log('Create conference with three calls.'); 1.981 + 1.982 + let outCall; 1.983 + let inCall; 1.984 + let inCall2; 1.985 + let outInfo = outCallStrPool(outNumber); 1.986 + let inInfo = inCallStrPool(inNumber); 1.987 + let inInfo2 = inCallStrPool(inNumber2); 1.988 + 1.989 + return Promise.resolve() 1.990 + .then(() => setupConferenceTwoCalls(outNumber, inNumber)) 1.991 + .then(calls => { 1.992 + outCall = calls[0]; 1.993 + inCall = calls[1]; 1.994 + }) 1.995 + .then(() => remoteDial(inNumber2)) 1.996 + .then(call => { inCall2 = call; }) 1.997 + .then(() => checkAll(conference, [inCall2], 'connected', [outCall, inCall], 1.998 + [outInfo.active, inInfo.active, inInfo2.incoming])) 1.999 + .then(() => answer(inCall2, function() { 1.1000 + checkState(inCall2, [inCall2], 'held', [outCall, inCall]); 1.1001 + })) 1.1002 + .then(() => checkAll(inCall2, [inCall2], 'held', [outCall, inCall], 1.1003 + [outInfo.held, inInfo.held, inInfo2.active])) 1.1004 + .then(() => addCallsToConference([inCall2], function() { 1.1005 + checkState(conference, [], 'connected', [outCall, inCall, inCall2]); 1.1006 + })) 1.1007 + .then(() => checkAll(conference, [], 1.1008 + 'connected', [outCall, inCall, inCall2], 1.1009 + [outInfo.active, inInfo.active, inInfo2.active])) 1.1010 + .then(() => { 1.1011 + return [outCall, inCall, inCall2]; 1.1012 + }); 1.1013 + } 1.1014 + 1.1015 + /** 1.1016 + * Setup a conference with an outgoing call and four incoming calls. 1.1017 + * 1.1018 + * @param outNumber 1.1019 + * Number of an outgoing call. 1.1020 + * @param inNumber 1.1021 + * Number of an incoming call. 1.1022 + * @param inNumber2 1.1023 + * Number of an incoming call. 1.1024 + * @param inNumber3 1.1025 + * Number of an incoming call. 1.1026 + * @param inNumber4 1.1027 + * Number of an incoming call. 1.1028 + * @return Promise<[outCall, inCall, inCall2, inCall3, inCall4]> 1.1029 + */ 1.1030 + function setupConferenceFiveCalls(outNumber, inNumber, inNumber2, inNumber3, 1.1031 + inNumber4) { 1.1032 + log('Create conference with five calls.'); 1.1033 + 1.1034 + let outCall; 1.1035 + let inCall; 1.1036 + let inCall2; 1.1037 + let inCall3; 1.1038 + let inCall4; 1.1039 + let outInfo = outCallStrPool(outNumber); 1.1040 + let inInfo = inCallStrPool(inNumber); 1.1041 + let inInfo2 = inCallStrPool(inNumber2); 1.1042 + let inInfo3 = inCallStrPool(inNumber3); 1.1043 + let inInfo4 = inCallStrPool(inNumber4); 1.1044 + 1.1045 + return Promise.resolve() 1.1046 + .then(() => setupConferenceThreeCalls(outNumber, inNumber, inNumber2)) 1.1047 + .then(calls => { 1.1048 + [outCall, inCall, inCall2] = calls; 1.1049 + }) 1.1050 + .then(() => remoteDial(inNumber3)) 1.1051 + .then(call => {inCall3 = call;}) 1.1052 + .then(() => checkAll(conference, [inCall3], 'connected', 1.1053 + [outCall, inCall, inCall2], 1.1054 + [outInfo.active, inInfo.active, inInfo2.active, 1.1055 + inInfo3.incoming])) 1.1056 + .then(() => answer(inCall3, function() { 1.1057 + checkState(inCall3, [inCall3], 'held', [outCall, inCall, inCall2]); 1.1058 + })) 1.1059 + .then(() => checkAll(inCall3, [inCall3], 'held', 1.1060 + [outCall, inCall, inCall2], 1.1061 + [outInfo.held, inInfo.held, inInfo2.held, 1.1062 + inInfo3.active])) 1.1063 + .then(() => addCallsToConference([inCall3], function() { 1.1064 + checkState(conference, [], 'connected', [outCall, inCall, inCall2, inCall3]); 1.1065 + })) 1.1066 + .then(() => checkAll(conference, [], 'connected', 1.1067 + [outCall, inCall, inCall2, inCall3], 1.1068 + [outInfo.active, inInfo.active, inInfo2.active, 1.1069 + inInfo3.active])) 1.1070 + .then(() => remoteDial(inNumber4)) 1.1071 + .then(call => {inCall4 = call;}) 1.1072 + .then(() => checkAll(conference, [inCall4], 'connected', 1.1073 + [outCall, inCall, inCall2, inCall3], 1.1074 + [outInfo.active, inInfo.active, inInfo2.active, 1.1075 + inInfo3.active, inInfo4.incoming])) 1.1076 + .then(() => answer(inCall4, function() { 1.1077 + checkState(inCall4, [inCall4], 'held', [outCall, inCall, inCall2, inCall3]); 1.1078 + })) 1.1079 + .then(() => checkAll(inCall4, [inCall4], 'held', 1.1080 + [outCall, inCall, inCall2, inCall3], 1.1081 + [outInfo.held, inInfo.held, inInfo2.held, 1.1082 + inInfo3.held, inInfo4.active])) 1.1083 + .then(() => addCallsToConference([inCall4], function() { 1.1084 + checkState(conference, [], 'connected', [outCall, inCall, inCall2, 1.1085 + inCall3, inCall4]); 1.1086 + })) 1.1087 + .then(() => checkAll(conference, [], 'connected', 1.1088 + [outCall, inCall, inCall2, inCall3, inCall4], 1.1089 + [outInfo.active, inInfo.active, inInfo2.active, 1.1090 + inInfo3.active, inInfo4.active])) 1.1091 + .then(() => { 1.1092 + return [outCall, inCall, inCall2, inCall3, inCall4]; 1.1093 + }); 1.1094 + } 1.1095 + 1.1096 + /** 1.1097 + * Public members. 1.1098 + */ 1.1099 + 1.1100 + this.gCheckInitialState = checkInitialState; 1.1101 + this.gClearCalls = clearCalls; 1.1102 + this.gOutCallStrPool = outCallStrPool; 1.1103 + this.gInCallStrPool = inCallStrPool; 1.1104 + this.gCheckState = checkState; 1.1105 + this.gCheckAll = checkAll; 1.1106 + this.gDial = dial; 1.1107 + this.gAnswer = answer; 1.1108 + this.gHold = hold; 1.1109 + this.gRemoteDial = remoteDial; 1.1110 + this.gRemoteAnswer = remoteAnswer; 1.1111 + this.gRemoteHangUp = remoteHangUp; 1.1112 + this.gRemoteHangUpCalls = remoteHangUpCalls; 1.1113 + this.gAddCallsToConference = addCallsToConference; 1.1114 + this.gHoldConference = holdConference; 1.1115 + this.gResumeConference = resumeConference; 1.1116 + this.gRemoveCallInConference = removeCallInConference; 1.1117 + this.gHangUpCallInConference = hangUpCallInConference; 1.1118 + this.gSetupConferenceTwoCalls = setupConferenceTwoCalls; 1.1119 + this.gSetupConferenceThreeCalls = setupConferenceThreeCalls; 1.1120 + this.gSetupConferenceFiveCalls = setupConferenceFiveCalls; 1.1121 + this.gReceivedPending = receivedPending; 1.1122 +}()); 1.1123 + 1.1124 +function _startTest(permissions, test) { 1.1125 + function permissionSetUp() { 1.1126 + SpecialPowers.setBoolPref("dom.mozSettings.enabled", true); 1.1127 + for (let per of permissions) { 1.1128 + SpecialPowers.addPermission(per, true, document); 1.1129 + } 1.1130 + } 1.1131 + 1.1132 + function permissionTearDown() { 1.1133 + SpecialPowers.clearUserPref("dom.mozSettings.enabled"); 1.1134 + for (let per of permissions) { 1.1135 + SpecialPowers.removePermission(per, document); 1.1136 + } 1.1137 + } 1.1138 + 1.1139 + function setUp() { 1.1140 + log("== Test SetUp =="); 1.1141 + permissionSetUp(); 1.1142 + // Make sure that we get the telephony after adding permission. 1.1143 + telephony = window.navigator.mozTelephony; 1.1144 + ok(telephony); 1.1145 + delayTelephonyDial(); 1.1146 + conference = telephony.conferenceGroup; 1.1147 + ok(conference); 1.1148 + return gClearCalls().then(gCheckInitialState); 1.1149 + } 1.1150 + 1.1151 + // Extend finish() with tear down. 1.1152 + finish = (function() { 1.1153 + let originalFinish = finish; 1.1154 + 1.1155 + function tearDown() { 1.1156 + log("== Test TearDown =="); 1.1157 + restoreTelephonyDial(); 1.1158 + emulator.waitFinish() 1.1159 + .then(permissionTearDown) 1.1160 + .then(function() { 1.1161 + originalFinish.apply(this, arguments); 1.1162 + }); 1.1163 + } 1.1164 + 1.1165 + return tearDown.bind(this); 1.1166 + }()); 1.1167 + 1.1168 + function mainTest() { 1.1169 + setUp() 1.1170 + .then(function onSuccess() { 1.1171 + log("== Test Start =="); 1.1172 + test(); 1.1173 + }, function onError(error) { 1.1174 + SpecialPowers.Cu.reportError(error); 1.1175 + ok(false, "SetUp error"); 1.1176 + }); 1.1177 + } 1.1178 + 1.1179 + mainTest(); 1.1180 +} 1.1181 + 1.1182 +function startTest(test) { 1.1183 + _startTest(["telephony"], test); 1.1184 +} 1.1185 + 1.1186 +function startTestWithPermissions(permissions, test) { 1.1187 + _startTest(permissions.concat("telephony"), test); 1.1188 +} 1.1189 + 1.1190 +function startDSDSTest(test) { 1.1191 + let numRIL; 1.1192 + try { 1.1193 + numRIL = SpecialPowers.getIntPref("ril.numRadioInterfaces"); 1.1194 + } catch (ex) { 1.1195 + numRIL = 1; // Pref not set. 1.1196 + } 1.1197 + 1.1198 + if (numRIL > 1) { 1.1199 + startTest(test); 1.1200 + } else { 1.1201 + log("Not a DSDS environment. Test is skipped."); 1.1202 + ok(true); // We should run at least one test. 1.1203 + finish(); 1.1204 + } 1.1205 +}