helpers.js 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342
  1. /*
  2. * Copyright 2012, Mozilla Foundation and contributors
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. "use strict";
  17. // A copy of this code exists in firefox mochitests. They should be kept
  18. // in sync. Hence the exports synonym for non AMD contexts.
  19. var { helpers, assert } = (function () {
  20. var helpers = {};
  21. var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
  22. var { TargetFactory } = require("devtools/client/framework/target");
  23. var Services = require("Services");
  24. var assert = { ok: ok, is: is, log: info };
  25. var util = require("gcli/util/util");
  26. var cli = require("gcli/cli");
  27. var KeyEvent = require("gcli/util/util").KeyEvent;
  28. const { GcliFront } = require("devtools/shared/fronts/gcli");
  29. /**
  30. * See notes in helpers.checkOptions()
  31. */
  32. var createDeveloperToolbarAutomator = function (toolbar) {
  33. var automator = {
  34. setInput: function (typed) {
  35. return toolbar.inputter.setInput(typed);
  36. },
  37. setCursor: function (cursor) {
  38. return toolbar.inputter.setCursor(cursor);
  39. },
  40. focus: function () {
  41. return toolbar.inputter.focus();
  42. },
  43. fakeKey: function (keyCode) {
  44. var fakeEvent = {
  45. keyCode: keyCode,
  46. preventDefault: function () { },
  47. timeStamp: new Date().getTime()
  48. };
  49. toolbar.inputter.onKeyDown(fakeEvent);
  50. if (keyCode === KeyEvent.DOM_VK_BACK_SPACE) {
  51. var input = toolbar.inputter.element;
  52. input.value = input.value.slice(0, -1);
  53. }
  54. return toolbar.inputter.handleKeyUp(fakeEvent);
  55. },
  56. getInputState: function () {
  57. return toolbar.inputter.getInputState();
  58. },
  59. getCompleterTemplateData: function () {
  60. return toolbar.completer._getCompleterTemplateData();
  61. },
  62. getErrorMessage: function () {
  63. return toolbar.tooltip.errorEle.textContent;
  64. }
  65. };
  66. Object.defineProperty(automator, "focusManager", {
  67. get: function () { return toolbar.focusManager; },
  68. enumerable: true
  69. });
  70. Object.defineProperty(automator, "field", {
  71. get: function () { return toolbar.tooltip.field; },
  72. enumerable: true
  73. });
  74. return automator;
  75. };
  76. /**
  77. * Warning: For use with Firefox Mochitests only.
  78. *
  79. * Open a new tab at a URL and call a callback on load, and then tidy up when
  80. * the callback finishes.
  81. * The function will be passed a set of test options, and will usually return a
  82. * promise to indicate that the tab can be cleared up. (To be formal, we call
  83. * Promise.resolve() on the return value of the callback function)
  84. *
  85. * The options used by addTab include:
  86. * - chromeWindow: XUL window parent of created tab. a.k.a 'window' in mochitest
  87. * - tab: The new XUL tab element, as returned by gBrowser.addTab()
  88. * - target: The debug target as defined by the devtools framework
  89. * - browser: The XUL browser element for the given tab
  90. * - isFirefox: Always true. Allows test sharing with GCLI
  91. *
  92. * Normally addTab will create an options object containing the values as
  93. * described above. However these options can be customized by the third
  94. * 'options' parameter. This has the ability to customize the value of
  95. * chromeWindow or isFirefox, and to add new properties.
  96. *
  97. * @param url The URL for the new tab
  98. * @param callback The function to call on page load
  99. * @param options An optional set of options to customize the way the tests run
  100. */
  101. helpers.addTab = function (url, callback, options) {
  102. waitForExplicitFinish();
  103. options = options || {};
  104. options.chromeWindow = options.chromeWindow || window;
  105. options.isFirefox = true;
  106. var tabbrowser = options.chromeWindow.gBrowser;
  107. options.tab = tabbrowser.addTab();
  108. tabbrowser.selectedTab = options.tab;
  109. options.browser = tabbrowser.getBrowserForTab(options.tab);
  110. options.target = TargetFactory.forTab(options.tab);
  111. var loaded = helpers.listenOnce(options.browser, "load", true).then(function (ev) {
  112. var reply = callback.call(null, options);
  113. return Promise.resolve(reply).then(null, function (error) {
  114. ok(false, error);
  115. }).then(function () {
  116. tabbrowser.removeTab(options.tab);
  117. delete options.target;
  118. delete options.browser;
  119. delete options.tab;
  120. delete options.chromeWindow;
  121. delete options.isFirefox;
  122. });
  123. });
  124. options.browser.contentWindow.location = url;
  125. return loaded;
  126. };
  127. /**
  128. * Open a new tab
  129. * @param url Address of the page to open
  130. * @param options Object to which we add properties describing the new tab. The
  131. * following properties are added:
  132. * - chromeWindow
  133. * - tab
  134. * - browser
  135. * - target
  136. * @return A promise which resolves to the options object when the 'load' event
  137. * happens on the new tab
  138. */
  139. helpers.openTab = function (url, options) {
  140. waitForExplicitFinish();
  141. options = options || {};
  142. options.chromeWindow = options.chromeWindow || window;
  143. options.isFirefox = true;
  144. var tabbrowser = options.chromeWindow.gBrowser;
  145. options.tab = tabbrowser.addTab();
  146. tabbrowser.selectedTab = options.tab;
  147. options.browser = tabbrowser.getBrowserForTab(options.tab);
  148. options.target = TargetFactory.forTab(options.tab);
  149. return helpers.navigate(url, options);
  150. };
  151. /**
  152. * Undo the effects of |helpers.openTab|
  153. * @param options The options object passed to |helpers.openTab|
  154. * @return A promise resolved (with undefined) when the tab is closed
  155. */
  156. helpers.closeTab = function (options) {
  157. options.chromeWindow.gBrowser.removeTab(options.tab);
  158. delete options.target;
  159. delete options.browser;
  160. delete options.tab;
  161. delete options.chromeWindow;
  162. delete options.isFirefox;
  163. return Promise.resolve(undefined);
  164. };
  165. /**
  166. * Open the developer toolbar in a tab
  167. * @param options Object to which we add properties describing the developer
  168. * toolbar. The following properties are added:
  169. * - automator
  170. * - requisition
  171. * @return A promise which resolves to the options object when the 'load' event
  172. * happens on the new tab
  173. */
  174. helpers.openToolbar = function (options) {
  175. options = options || {};
  176. options.chromeWindow = options.chromeWindow || window;
  177. return options.chromeWindow.DeveloperToolbar.show(true).then(function () {
  178. var toolbar = options.chromeWindow.DeveloperToolbar;
  179. options.automator = createDeveloperToolbarAutomator(toolbar);
  180. options.requisition = toolbar.requisition;
  181. return options;
  182. });
  183. };
  184. /**
  185. * Navigate the current tab to a URL
  186. */
  187. helpers.navigate = Task.async(function* (url, options) {
  188. options = options || {};
  189. options.chromeWindow = options.chromeWindow || window;
  190. options.tab = options.tab || options.chromeWindow.gBrowser.selectedTab;
  191. var tabbrowser = options.chromeWindow.gBrowser;
  192. options.browser = tabbrowser.getBrowserForTab(options.tab);
  193. let onLoaded = BrowserTestUtils.browserLoaded(options.browser);
  194. options.browser.loadURI(url);
  195. yield onLoaded;
  196. return options;
  197. });
  198. /**
  199. * Undo the effects of |helpers.openToolbar|
  200. * @param options The options object passed to |helpers.openToolbar|
  201. * @return A promise resolved (with undefined) when the toolbar is closed
  202. */
  203. helpers.closeToolbar = function (options) {
  204. return options.chromeWindow.DeveloperToolbar.hide().then(function () {
  205. delete options.automator;
  206. delete options.requisition;
  207. });
  208. };
  209. /**
  210. * A helper to work with Task.spawn so you can do:
  211. * return Task.spawn(realTestFunc).then(finish, helpers.handleError);
  212. */
  213. helpers.handleError = function (ex) {
  214. console.error(ex);
  215. ok(false, ex);
  216. finish();
  217. };
  218. /**
  219. * A helper for calling addEventListener and then removeEventListener as soon
  220. * as the event is called, passing the results on as a promise
  221. * @param element The DOM element to listen on
  222. * @param event The name of the event to listen for
  223. * @param useCapture Should we use the capturing phase?
  224. * @return A promise resolved with the event object when the event first happens
  225. */
  226. helpers.listenOnce = function (element, event, useCapture) {
  227. return new Promise(function (resolve, reject) {
  228. var onEvent = function (ev) {
  229. element.removeEventListener(event, onEvent, useCapture);
  230. resolve(ev);
  231. };
  232. element.addEventListener(event, onEvent, useCapture);
  233. }.bind(this));
  234. };
  235. /**
  236. * A wrapper for calling Services.obs.[add|remove]Observer using promises.
  237. * @param topic The topic parameter to Services.obs.addObserver
  238. * @param ownsWeak The ownsWeak parameter to Services.obs.addObserver with a
  239. * default value of false
  240. * @return a promise that resolves when the ObserverService first notifies us
  241. * of the topic. The value of the promise is the first parameter to the observer
  242. * function other parameters are dropped.
  243. */
  244. helpers.observeOnce = function (topic, ownsWeak = false) {
  245. return new Promise(function (resolve, reject) {
  246. let resolver = function (subject) {
  247. Services.obs.removeObserver(resolver, topic);
  248. resolve(subject);
  249. };
  250. Services.obs.addObserver(resolver, topic, ownsWeak);
  251. }.bind(this));
  252. };
  253. /**
  254. * Takes a function that uses a callback as its last parameter, and returns a
  255. * new function that returns a promise instead
  256. */
  257. helpers.promiseify = function (functionWithLastParamCallback, scope) {
  258. return function () {
  259. let args = [].slice.call(arguments);
  260. return new Promise(resolve => {
  261. args.push((...results) => {
  262. resolve(results.length > 1 ? results : results[0]);
  263. });
  264. functionWithLastParamCallback.apply(scope, args);
  265. });
  266. };
  267. };
  268. /**
  269. * Warning: For use with Firefox Mochitests only.
  270. *
  271. * As addTab, but that also opens the developer toolbar. In addition a new
  272. * 'automator' property is added to the options object which uses the
  273. * developer toolbar
  274. */
  275. helpers.addTabWithToolbar = function (url, callback, options) {
  276. return helpers.addTab(url, function (innerOptions) {
  277. var win = innerOptions.chromeWindow;
  278. return win.DeveloperToolbar.show(true).then(function () {
  279. var toolbar = win.DeveloperToolbar;
  280. innerOptions.automator = createDeveloperToolbarAutomator(toolbar);
  281. innerOptions.requisition = toolbar.requisition;
  282. var reply = callback.call(null, innerOptions);
  283. return Promise.resolve(reply).then(null, function (error) {
  284. ok(false, error);
  285. console.error(error);
  286. }).then(function () {
  287. win.DeveloperToolbar.hide().then(function () {
  288. delete innerOptions.automator;
  289. });
  290. });
  291. });
  292. }, options);
  293. };
  294. /**
  295. * Warning: For use with Firefox Mochitests only.
  296. *
  297. * Run a set of test functions stored in the values of the 'exports' object
  298. * functions stored under setup/shutdown will be run at the start/end of the
  299. * sequence of tests.
  300. * A test will be considered finished when its return value is resolved.
  301. * @param options An object to be passed to the test functions
  302. * @param tests An object containing named test functions
  303. * @return a promise which will be resolved when all tests have been run and
  304. * their return values resolved
  305. */
  306. helpers.runTests = function (options, tests) {
  307. var testNames = Object.keys(tests).filter(function (test) {
  308. return test != "setup" && test != "shutdown";
  309. });
  310. var recover = function (error) {
  311. ok(false, error);
  312. console.error(error, error.stack);
  313. };
  314. info("SETUP");
  315. var setupDone = (tests.setup != null) ?
  316. Promise.resolve(tests.setup(options)) :
  317. Promise.resolve();
  318. var testDone = setupDone.then(function () {
  319. return util.promiseEach(testNames, function (testName) {
  320. info(testName);
  321. var action = tests[testName];
  322. if (typeof action === "function") {
  323. var reply = action.call(tests, options);
  324. return Promise.resolve(reply);
  325. }
  326. else if (Array.isArray(action)) {
  327. return helpers.audit(options, action);
  328. }
  329. return Promise.reject("test action '" + testName +
  330. "' is not a function or helpers.audit() object");
  331. });
  332. }, recover);
  333. return testDone.then(function () {
  334. info("SHUTDOWN");
  335. return (tests.shutdown != null) ?
  336. Promise.resolve(tests.shutdown(options)) :
  337. Promise.resolve();
  338. }, recover);
  339. };
  340. const MOCK_COMMANDS_URI = "chrome://mochitests/content/browser/devtools/client/commandline/test/mockCommands.js";
  341. const defer = function () {
  342. const deferred = { };
  343. deferred.promise = new Promise(function (resolve, reject) {
  344. deferred.resolve = resolve;
  345. deferred.reject = reject;
  346. });
  347. return deferred;
  348. };
  349. /**
  350. * This does several actions associated with running a GCLI test in mochitest
  351. * 1. Create a new tab containing basic markup for GCLI tests
  352. * 2. Open the developer toolbar
  353. * 3. Register the mock commands with the server process
  354. * 4. Wait for the proxy commands to be auto-regitstered with the client
  355. * 5. Register the mock converters with the client process
  356. * 6. Run all the tests
  357. * 7. Tear down all the setup
  358. */
  359. helpers.runTestModule = function (exports, name) {
  360. return Task.spawn(function* () {
  361. const uri = "data:text/html;charset=utf-8," +
  362. "<style>div{color:red;}</style>" +
  363. "<div id='gcli-root'>" + name + "</div>";
  364. const options = yield helpers.openTab(uri);
  365. options.isRemote = true;
  366. yield helpers.openToolbar(options);
  367. const system = options.requisition.system;
  368. // Register a one time listener with the local set of commands
  369. const addedDeferred = defer();
  370. const removedDeferred = defer();
  371. let state = "preAdd"; // Then 'postAdd' then 'postRemove'
  372. system.commands.onCommandsChange.add(function (ev) {
  373. if (system.commands.get("tsslow") != null) {
  374. if (state === "preAdd") {
  375. addedDeferred.resolve();
  376. state = "postAdd";
  377. }
  378. }
  379. else {
  380. if (state === "postAdd") {
  381. removedDeferred.resolve();
  382. state = "postRemove";
  383. }
  384. }
  385. });
  386. // Send a message to add the commands to the content process
  387. const front = yield GcliFront.create(options.target);
  388. yield front._testOnlyAddItemsByModule(MOCK_COMMANDS_URI);
  389. // This will cause the local set of commands to be updated with the
  390. // command proxies, wait for that to complete.
  391. yield addedDeferred.promise;
  392. // Now we need to add the converters to the local GCLI
  393. const converters = mockCommands.items.filter(item => item.item === "converter");
  394. system.addItems(converters);
  395. // Next run the tests
  396. yield helpers.runTests(options, exports);
  397. // Finally undo the mock commands and converters
  398. system.removeItems(converters);
  399. const removePromise = system.commands.onCommandsChange.once();
  400. yield front._testOnlyRemoveItemsByModule(MOCK_COMMANDS_URI);
  401. yield removedDeferred.promise;
  402. // And close everything down
  403. yield helpers.closeToolbar(options);
  404. yield helpers.closeTab(options);
  405. }).then(finish, helpers.handleError);
  406. };
  407. /**
  408. * Ensure that the options object is setup correctly
  409. * options should contain an automator object that looks like this:
  410. * {
  411. * getInputState: function() { ... },
  412. * setCursor: function(cursor) { ... },
  413. * getCompleterTemplateData: function() { ... },
  414. * focus: function() { ... },
  415. * getErrorMessage: function() { ... },
  416. * fakeKey: function(keyCode) { ... },
  417. * setInput: function(typed) { ... },
  418. * focusManager: ...,
  419. * field: ...,
  420. * }
  421. */
  422. function checkOptions(options) {
  423. if (options == null) {
  424. console.trace();
  425. throw new Error("Missing options object");
  426. }
  427. if (options.requisition == null) {
  428. console.trace();
  429. throw new Error("options.requisition == null");
  430. }
  431. }
  432. /**
  433. * Various functions to return the actual state of the command line
  434. */
  435. helpers._actual = {
  436. input: function (options) {
  437. return options.automator.getInputState().typed;
  438. },
  439. hints: function (options) {
  440. return options.automator.getCompleterTemplateData().then(function (data) {
  441. var emptyParams = data.emptyParameters.join("");
  442. return (data.directTabText + emptyParams + data.arrowTabText)
  443. .replace(/\u00a0/g, " ")
  444. .replace(/\u21E5/, "->")
  445. .replace(/ $/, "");
  446. });
  447. },
  448. markup: function (options) {
  449. var cursor = helpers._actual.cursor(options);
  450. var statusMarkup = options.requisition.getInputStatusMarkup(cursor);
  451. return statusMarkup.map(function (s) {
  452. return new Array(s.string.length + 1).join(s.status.toString()[0]);
  453. }).join("");
  454. },
  455. cursor: function (options) {
  456. return options.automator.getInputState().cursor.start;
  457. },
  458. current: function (options) {
  459. var cursor = helpers._actual.cursor(options);
  460. return options.requisition.getAssignmentAt(cursor).param.name;
  461. },
  462. status: function (options) {
  463. return options.requisition.status.toString();
  464. },
  465. predictions: function (options) {
  466. var cursor = helpers._actual.cursor(options);
  467. var assignment = options.requisition.getAssignmentAt(cursor);
  468. var context = options.requisition.executionContext;
  469. return assignment.getPredictions(context).then(function (predictions) {
  470. return predictions.map(function (prediction) {
  471. return prediction.name;
  472. });
  473. });
  474. },
  475. unassigned: function (options) {
  476. return options.requisition._unassigned.map(function (assignment) {
  477. return assignment.arg.toString();
  478. }.bind(this));
  479. },
  480. outputState: function (options) {
  481. var outputData = options.automator.focusManager._shouldShowOutput();
  482. return outputData.visible + ":" + outputData.reason;
  483. },
  484. tooltipState: function (options) {
  485. var tooltipData = options.automator.focusManager._shouldShowTooltip();
  486. return tooltipData.visible + ":" + tooltipData.reason;
  487. },
  488. options: function (options) {
  489. if (options.automator.field.menu == null) {
  490. return [];
  491. }
  492. return options.automator.field.menu.items.map(function (item) {
  493. return item.name.textContent ? item.name.textContent : item.name;
  494. });
  495. },
  496. message: function (options) {
  497. return options.automator.getErrorMessage();
  498. }
  499. };
  500. function shouldOutputUnquoted(value) {
  501. var type = typeof value;
  502. return value == null || type === "boolean" || type === "number";
  503. }
  504. function outputArray(array) {
  505. return (array.length === 0) ?
  506. "[ ]" :
  507. "[ '" + array.join("', '") + "' ]";
  508. }
  509. helpers._createDebugCheck = function (options) {
  510. checkOptions(options);
  511. var requisition = options.requisition;
  512. var command = requisition.commandAssignment.value;
  513. var cursor = helpers._actual.cursor(options);
  514. var input = helpers._actual.input(options);
  515. var padding = new Array(input.length + 1).join(" ");
  516. var hintsPromise = helpers._actual.hints(options);
  517. var predictionsPromise = helpers._actual.predictions(options);
  518. return Promise.all([ hintsPromise, predictionsPromise ]).then(function (values) {
  519. var hints = values[0];
  520. var predictions = values[1];
  521. var output = "";
  522. output += "return helpers.audit(options, [\n";
  523. output += " {\n";
  524. if (cursor === input.length) {
  525. output += " setup: '" + input + "',\n";
  526. }
  527. else {
  528. output += " name: '" + input + " (cursor=" + cursor + ")',\n";
  529. output += " setup: function() {\n";
  530. output += " return helpers.setInput(options, '" + input + "', " + cursor + ");\n";
  531. output += " },\n";
  532. }
  533. output += " check: {\n";
  534. output += " input: '" + input + "',\n";
  535. output += " hints: " + padding + "'" + hints + "',\n";
  536. output += " markup: '" + helpers._actual.markup(options) + "',\n";
  537. output += " cursor: " + cursor + ",\n";
  538. output += " current: '" + helpers._actual.current(options) + "',\n";
  539. output += " status: '" + helpers._actual.status(options) + "',\n";
  540. output += " options: " + outputArray(helpers._actual.options(options)) + ",\n";
  541. output += " message: '" + helpers._actual.message(options) + "',\n";
  542. output += " predictions: " + outputArray(predictions) + ",\n";
  543. output += " unassigned: " + outputArray(requisition._unassigned) + ",\n";
  544. output += " outputState: '" + helpers._actual.outputState(options) + "',\n";
  545. output += " tooltipState: '" + helpers._actual.tooltipState(options) + "'" +
  546. (command ? "," : "") + "\n";
  547. if (command) {
  548. output += " args: {\n";
  549. output += " command: { name: '" + command.name + "' },\n";
  550. requisition.getAssignments().forEach(function (assignment) {
  551. output += " " + assignment.param.name + ": { ";
  552. if (typeof assignment.value === "string") {
  553. output += "value: '" + assignment.value + "', ";
  554. }
  555. else if (shouldOutputUnquoted(assignment.value)) {
  556. output += "value: " + assignment.value + ", ";
  557. }
  558. else {
  559. output += "/*value:" + assignment.value + ",*/ ";
  560. }
  561. output += "arg: '" + assignment.arg + "', ";
  562. output += "status: '" + assignment.getStatus().toString() + "', ";
  563. output += "message: '" + assignment.message + "'";
  564. output += " },\n";
  565. });
  566. output += " }\n";
  567. }
  568. output += " },\n";
  569. output += " exec: {\n";
  570. output += " output: '',\n";
  571. output += " type: 'string',\n";
  572. output += " error: false\n";
  573. output += " }\n";
  574. output += " }\n";
  575. output += "]);";
  576. return output;
  577. }.bind(this), util.errorHandler);
  578. };
  579. /**
  580. * Simulate focusing the input field
  581. */
  582. helpers.focusInput = function (options) {
  583. checkOptions(options);
  584. options.automator.focus();
  585. };
  586. /**
  587. * Simulate pressing TAB in the input field
  588. */
  589. helpers.pressTab = function (options) {
  590. checkOptions(options);
  591. return helpers.pressKey(options, KeyEvent.DOM_VK_TAB);
  592. };
  593. /**
  594. * Simulate pressing RETURN in the input field
  595. */
  596. helpers.pressReturn = function (options) {
  597. checkOptions(options);
  598. return helpers.pressKey(options, KeyEvent.DOM_VK_RETURN);
  599. };
  600. /**
  601. * Simulate pressing a key by keyCode in the input field
  602. */
  603. helpers.pressKey = function (options, keyCode) {
  604. checkOptions(options);
  605. return options.automator.fakeKey(keyCode);
  606. };
  607. /**
  608. * A list of special key presses and how to to them, for the benefit of
  609. * helpers.setInput
  610. */
  611. var ACTIONS = {
  612. "<TAB>": function (options) {
  613. return helpers.pressTab(options);
  614. },
  615. "<RETURN>": function (options) {
  616. return helpers.pressReturn(options);
  617. },
  618. "<UP>": function (options) {
  619. return helpers.pressKey(options, KeyEvent.DOM_VK_UP);
  620. },
  621. "<DOWN>": function (options) {
  622. return helpers.pressKey(options, KeyEvent.DOM_VK_DOWN);
  623. },
  624. "<BACKSPACE>": function (options) {
  625. return helpers.pressKey(options, KeyEvent.DOM_VK_BACK_SPACE);
  626. }
  627. };
  628. /**
  629. * Used in helpers.setInput to cut an input string like 'blah<TAB>foo<UP>' into
  630. * an array like [ 'blah', '<TAB>', 'foo', '<UP>' ].
  631. * When using this RegExp, you also need to filter out the blank strings.
  632. */
  633. var CHUNKER = /([^<]*)(<[A-Z]+>)/;
  634. /**
  635. * Alter the input to <code>typed</code> optionally leaving the cursor at
  636. * <code>cursor</code>.
  637. * @return A promise of the number of key-presses to respond
  638. */
  639. helpers.setInput = function (options, typed, cursor) {
  640. checkOptions(options);
  641. var inputPromise;
  642. var automator = options.automator;
  643. // We try to measure average keypress time, but setInput can simulate
  644. // several, so we try to keep track of how many
  645. var chunkLen = 1;
  646. // The easy case is a simple string without things like <TAB>
  647. if (typed.indexOf("<") === -1) {
  648. inputPromise = automator.setInput(typed);
  649. }
  650. else {
  651. // Cut the input up into input strings separated by '<KEY>' tokens. The
  652. // CHUNKS RegExp leaves blanks so we filter them out.
  653. var chunks = typed.split(CHUNKER).filter(function (s) {
  654. return s !== "";
  655. });
  656. chunkLen = chunks.length + 1;
  657. // We're working on this in chunks so first clear the input
  658. inputPromise = automator.setInput("").then(function () {
  659. return util.promiseEach(chunks, function (chunk) {
  660. if (chunk.charAt(0) === "<") {
  661. var action = ACTIONS[chunk];
  662. if (typeof action !== "function") {
  663. console.error("Known actions: " + Object.keys(ACTIONS).join());
  664. throw new Error('Key action not found "' + chunk + '"');
  665. }
  666. return action(options);
  667. }
  668. else {
  669. return automator.setInput(automator.getInputState().typed + chunk);
  670. }
  671. });
  672. });
  673. }
  674. return inputPromise.then(function () {
  675. if (cursor != null) {
  676. automator.setCursor({ start: cursor, end: cursor });
  677. }
  678. if (automator.focusManager) {
  679. automator.focusManager.onInputChange();
  680. }
  681. // Firefox testing is noisy and distant, so logging helps
  682. if (options.isFirefox) {
  683. var cursorStr = (cursor == null ? "" : ", " + cursor);
  684. log('setInput("' + typed + '"' + cursorStr + ")");
  685. }
  686. return chunkLen;
  687. });
  688. };
  689. /**
  690. * Helper for helpers.audit() to ensure that all the 'check' properties match.
  691. * See helpers.audit for more information.
  692. * @param name The name to use in error messages
  693. * @param checks See helpers.audit for a list of available checks
  694. * @return A promise which resolves to undefined when the checks are complete
  695. */
  696. helpers._check = function (options, name, checks) {
  697. // A test method to check that all args are assigned in some way
  698. var requisition = options.requisition;
  699. requisition._args.forEach(function (arg) {
  700. if (arg.assignment == null) {
  701. assert.ok(false, "No assignment for " + arg);
  702. }
  703. });
  704. if (checks == null) {
  705. return Promise.resolve();
  706. }
  707. var outstanding = [];
  708. var suffix = name ? " (for '" + name + "')" : "";
  709. if (!options.isNode && "input" in checks) {
  710. assert.is(helpers._actual.input(options), checks.input, "input" + suffix);
  711. }
  712. if (!options.isNode && "cursor" in checks) {
  713. assert.is(helpers._actual.cursor(options), checks.cursor, "cursor" + suffix);
  714. }
  715. if (!options.isNode && "current" in checks) {
  716. assert.is(helpers._actual.current(options), checks.current, "current" + suffix);
  717. }
  718. if ("status" in checks) {
  719. assert.is(helpers._actual.status(options), checks.status, "status" + suffix);
  720. }
  721. if (!options.isNode && "markup" in checks) {
  722. assert.is(helpers._actual.markup(options), checks.markup, "markup" + suffix);
  723. }
  724. if (!options.isNode && "hints" in checks) {
  725. var hintCheck = function (actualHints) {
  726. assert.is(actualHints, checks.hints, "hints" + suffix);
  727. };
  728. outstanding.push(helpers._actual.hints(options).then(hintCheck));
  729. }
  730. if (!options.isNode && "predictions" in checks) {
  731. var predictionsCheck = function (actualPredictions) {
  732. helpers.arrayIs(actualPredictions,
  733. checks.predictions,
  734. "predictions" + suffix);
  735. };
  736. outstanding.push(helpers._actual.predictions(options).then(predictionsCheck));
  737. }
  738. if (!options.isNode && "predictionsContains" in checks) {
  739. var containsCheck = function (actualPredictions) {
  740. checks.predictionsContains.forEach(function (prediction) {
  741. var index = actualPredictions.indexOf(prediction);
  742. assert.ok(index !== -1,
  743. "predictionsContains:" + prediction + suffix);
  744. if (index === -1) {
  745. log("Actual predictions (" + actualPredictions.length + "): " +
  746. actualPredictions.join(", "));
  747. }
  748. });
  749. };
  750. outstanding.push(helpers._actual.predictions(options).then(containsCheck));
  751. }
  752. if ("unassigned" in checks) {
  753. helpers.arrayIs(helpers._actual.unassigned(options),
  754. checks.unassigned,
  755. "unassigned" + suffix);
  756. }
  757. /* TODO: Fix this
  758. if (!options.isNode && 'tooltipState' in checks) {
  759. assert.is(helpers._actual.tooltipState(options),
  760. checks.tooltipState,
  761. 'tooltipState' + suffix);
  762. }
  763. */
  764. if (!options.isNode && "outputState" in checks) {
  765. assert.is(helpers._actual.outputState(options),
  766. checks.outputState,
  767. "outputState" + suffix);
  768. }
  769. if (!options.isNode && "options" in checks) {
  770. helpers.arrayIs(helpers._actual.options(options),
  771. checks.options,
  772. "options" + suffix);
  773. }
  774. if (!options.isNode && "error" in checks) {
  775. assert.is(helpers._actual.message(options), checks.error, "error" + suffix);
  776. }
  777. if (checks.args != null) {
  778. Object.keys(checks.args).forEach(function (paramName) {
  779. var check = checks.args[paramName];
  780. // We allow an 'argument' called 'command' to be the command itself, but
  781. // what if the command has a parameter called 'command' (for example, an
  782. // 'exec' command)? We default to using the parameter because checking
  783. // the command value is less useful
  784. var assignment = requisition.getAssignment(paramName);
  785. if (assignment == null && paramName === "command") {
  786. assignment = requisition.commandAssignment;
  787. }
  788. if (assignment == null) {
  789. assert.ok(false, "Unknown arg: " + paramName + suffix);
  790. return;
  791. }
  792. if ("value" in check) {
  793. if (typeof check.value === "function") {
  794. try {
  795. check.value(assignment.value);
  796. }
  797. catch (ex) {
  798. assert.ok(false, "" + ex);
  799. }
  800. }
  801. else {
  802. assert.is(assignment.value,
  803. check.value,
  804. "arg." + paramName + ".value" + suffix);
  805. }
  806. }
  807. if ("name" in check) {
  808. assert.is(assignment.value.name,
  809. check.name,
  810. "arg." + paramName + ".name" + suffix);
  811. }
  812. if ("type" in check) {
  813. assert.is(assignment.arg.type,
  814. check.type,
  815. "arg." + paramName + ".type" + suffix);
  816. }
  817. if ("arg" in check) {
  818. assert.is(assignment.arg.toString(),
  819. check.arg,
  820. "arg." + paramName + ".arg" + suffix);
  821. }
  822. if ("status" in check) {
  823. assert.is(assignment.getStatus().toString(),
  824. check.status,
  825. "arg." + paramName + ".status" + suffix);
  826. }
  827. if (!options.isNode && "message" in check) {
  828. if (typeof check.message.test === "function") {
  829. assert.ok(check.message.test(assignment.message),
  830. "arg." + paramName + ".message" + suffix);
  831. }
  832. else {
  833. assert.is(assignment.message,
  834. check.message,
  835. "arg." + paramName + ".message" + suffix);
  836. }
  837. }
  838. });
  839. }
  840. return Promise.all(outstanding).then(function () {
  841. // Ensure the promise resolves to nothing
  842. return undefined;
  843. });
  844. };
  845. /**
  846. * Helper for helpers.audit() to ensure that all the 'exec' properties work.
  847. * See helpers.audit for more information.
  848. * @param name The name to use in error messages
  849. * @param expected See helpers.audit for a list of available exec checks
  850. * @return A promise which resolves to undefined when the checks are complete
  851. */
  852. helpers._exec = function (options, name, expected) {
  853. var requisition = options.requisition;
  854. if (expected == null) {
  855. return Promise.resolve({});
  856. }
  857. var origLogErrors = cli.logErrors;
  858. if (expected.error) {
  859. cli.logErrors = false;
  860. }
  861. try {
  862. return requisition.exec({ hidden: true }).then(function (output) {
  863. if ("type" in expected) {
  864. assert.is(output.type,
  865. expected.type,
  866. "output.type for: " + name);
  867. }
  868. if ("error" in expected) {
  869. assert.is(output.error,
  870. expected.error,
  871. "output.error for: " + name);
  872. }
  873. if (!("output" in expected)) {
  874. return { output: output };
  875. }
  876. var context = requisition.conversionContext;
  877. var convertPromise;
  878. if (options.isNode) {
  879. convertPromise = output.convert("string", context);
  880. }
  881. else {
  882. convertPromise = output.convert("dom", context).then(function (node) {
  883. return (node == null) ? "" : node.textContent.trim();
  884. });
  885. }
  886. return convertPromise.then(function (textOutput) {
  887. var doTest = function (match, against) {
  888. // Only log the real textContent if the test fails
  889. if (against.match(match) != null) {
  890. assert.ok(true, "html output for '" + name + "' " +
  891. "should match /" + (match.source || match) + "/");
  892. } else {
  893. assert.ok(false, "html output for '" + name + "' " +
  894. "should match /" + (match.source || match) + "/. " +
  895. 'Actual textContent: "' + against + '"');
  896. }
  897. };
  898. if (typeof expected.output === "string") {
  899. assert.is(textOutput,
  900. expected.output,
  901. "html output for " + name);
  902. }
  903. else if (Array.isArray(expected.output)) {
  904. expected.output.forEach(function (match) {
  905. doTest(match, textOutput);
  906. });
  907. }
  908. else {
  909. doTest(expected.output, textOutput);
  910. }
  911. if (expected.error) {
  912. cli.logErrors = origLogErrors;
  913. }
  914. return { output: output, text: textOutput };
  915. });
  916. }.bind(this)).then(function (data) {
  917. if (expected.error) {
  918. cli.logErrors = origLogErrors;
  919. }
  920. return data;
  921. });
  922. }
  923. catch (ex) {
  924. assert.ok(false, "Failure executing '" + name + "': " + ex);
  925. util.errorHandler(ex);
  926. if (expected.error) {
  927. cli.logErrors = origLogErrors;
  928. }
  929. return Promise.resolve({});
  930. }
  931. };
  932. /**
  933. * Helper to setup the test
  934. */
  935. helpers._setup = function (options, name, audit) {
  936. if (typeof audit.setup === "string") {
  937. return helpers.setInput(options, audit.setup);
  938. }
  939. if (typeof audit.setup === "function") {
  940. return Promise.resolve(audit.setup.call(audit));
  941. }
  942. return Promise.reject("'setup' property must be a string or a function. Is " + audit.setup);
  943. };
  944. /**
  945. * Helper to shutdown the test
  946. */
  947. helpers._post = function (name, audit, data) {
  948. if (typeof audit.post === "function") {
  949. return Promise.resolve(audit.post.call(audit, data.output, data.text));
  950. }
  951. return Promise.resolve(audit.post);
  952. };
  953. /*
  954. * We do some basic response time stats so we can see if we're getting slow
  955. */
  956. var totalResponseTime = 0;
  957. var averageOver = 0;
  958. var maxResponseTime = 0;
  959. var maxResponseCulprit;
  960. var start;
  961. /**
  962. * Restart the stats collection process
  963. */
  964. helpers.resetResponseTimes = function () {
  965. start = new Date().getTime();
  966. totalResponseTime = 0;
  967. averageOver = 0;
  968. maxResponseTime = 0;
  969. maxResponseCulprit = undefined;
  970. };
  971. /**
  972. * Expose an average response time in milliseconds
  973. */
  974. Object.defineProperty(helpers, "averageResponseTime", {
  975. get: function () {
  976. return averageOver === 0 ?
  977. undefined :
  978. Math.round(100 * totalResponseTime / averageOver) / 100;
  979. },
  980. enumerable: true
  981. });
  982. /**
  983. * Expose a maximum response time in milliseconds
  984. */
  985. Object.defineProperty(helpers, "maxResponseTime", {
  986. get: function () { return Math.round(maxResponseTime * 100) / 100; },
  987. enumerable: true
  988. });
  989. /**
  990. * Expose the name of the test that provided the maximum response time
  991. */
  992. Object.defineProperty(helpers, "maxResponseCulprit", {
  993. get: function () { return maxResponseCulprit; },
  994. enumerable: true
  995. });
  996. /**
  997. * Quick summary of the times
  998. */
  999. Object.defineProperty(helpers, "timingSummary", {
  1000. get: function () {
  1001. var elapsed = (new Date().getTime() - start) / 1000;
  1002. return "Total " + elapsed + "s, " +
  1003. "ave response " + helpers.averageResponseTime + "ms, " +
  1004. "max response " + helpers.maxResponseTime + "ms " +
  1005. "from '" + helpers.maxResponseCulprit + "'";
  1006. },
  1007. enumerable: true
  1008. });
  1009. /**
  1010. * A way of turning a set of tests into something more declarative, this helps
  1011. * to allow tests to be asynchronous.
  1012. * @param audits An array of objects each of which contains:
  1013. * - setup: string/function to be called to set the test up.
  1014. * If audit is a string then it is passed to helpers.setInput().
  1015. * If audit is a function then it is executed. The tests will wait while
  1016. * tests that return promises complete.
  1017. * - name: For debugging purposes. If name is undefined, and 'setup'
  1018. * is a string then the setup value will be used automatically
  1019. * - skipIf: A function to define if the test should be skipped. Useful for
  1020. * excluding tests from certain environments (e.g. nodom, firefox, etc).
  1021. * The name of the test will be used in log messages noting the skip
  1022. * See helpers.reason for pre-defined skip functions. The skip function must
  1023. * be synchronous, and will be passed the test options object.
  1024. * - skipRemainingIf: A function to skip all the remaining audits in this set.
  1025. * See skipIf for details of how skip functions work.
  1026. * - check: Check data. Available checks:
  1027. * - input: The text displayed in the input field
  1028. * - cursor: The position of the start of the cursor
  1029. * - status: One of 'VALID', 'ERROR', 'INCOMPLETE'
  1030. * - hints: The hint text, i.e. a concatenation of the directTabText, the
  1031. * emptyParameters and the arrowTabText. The text as inserted into the UI
  1032. * will include NBSP and Unicode RARR characters, these should be
  1033. * represented using normal space and '->' for the arrow
  1034. * - markup: What state should the error markup be in. e.g. 'VVVIIIEEE'
  1035. * - args: Maps of checks to make against the arguments:
  1036. * - value: i.e. assignment.value (which ignores defaultValue)
  1037. * - type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned
  1038. * Care should be taken with this since it's something of an
  1039. * implementation detail
  1040. * - arg: The toString value of the argument
  1041. * - status: i.e. assignment.getStatus
  1042. * - message: i.e. assignment.message
  1043. * - name: For commands - checks assignment.value.name
  1044. * - exec: Object to indicate we should execute the command and check the
  1045. * results. Available checks:
  1046. * - output: A string, RegExp or array of RegExps to compare with the output
  1047. * If typeof output is a string then the output should be exactly equal
  1048. * to the given string. If the type of output is a RegExp or array of
  1049. * RegExps then the output should match all RegExps
  1050. * - error: If true, then it is expected that this command will fail (that
  1051. * is, return a rejected promise or throw an exception)
  1052. * - type: A string documenting the expected type of the return value
  1053. * - post: Function to be called after the checks have been run, which will be
  1054. * passed 2 parameters: the first being output data (with type, data, and
  1055. * error properties), and the second being the converted text version of
  1056. * the output data
  1057. */
  1058. helpers.audit = function (options, audits) {
  1059. checkOptions(options);
  1060. var skipReason = null;
  1061. return util.promiseEach(audits, function (audit) {
  1062. var name = audit.name;
  1063. if (name == null && typeof audit.setup === "string") {
  1064. name = audit.setup;
  1065. }
  1066. if (assert.testLogging) {
  1067. log("- START '" + name + "' in " + assert.currentTest);
  1068. }
  1069. if (audit.skipRemainingIf) {
  1070. var skipRemainingIf = (typeof audit.skipRemainingIf === "function") ?
  1071. audit.skipRemainingIf(options) :
  1072. !!audit.skipRemainingIf;
  1073. if (skipRemainingIf) {
  1074. skipReason = audit.skipRemainingIf.name ?
  1075. "due to " + audit.skipRemainingIf.name :
  1076. "";
  1077. assert.log("Skipped " + name + " " + skipReason);
  1078. // Tests need at least one pass, fail or todo. Create a dummy pass
  1079. assert.ok(true, "Each test requires at least one pass, fail or todo");
  1080. return Promise.resolve(undefined);
  1081. }
  1082. }
  1083. if (audit.skipIf) {
  1084. var skip = (typeof audit.skipIf === "function") ?
  1085. audit.skipIf(options) :
  1086. !!audit.skipIf;
  1087. if (skip) {
  1088. var reason = audit.skipIf.name ? "due to " + audit.skipIf.name : "";
  1089. assert.log("Skipped " + name + " " + reason);
  1090. return Promise.resolve(undefined);
  1091. }
  1092. }
  1093. if (skipReason != null) {
  1094. assert.log("Skipped " + name + " " + skipReason);
  1095. return Promise.resolve(undefined);
  1096. }
  1097. var start = new Date().getTime();
  1098. var setupDone = helpers._setup(options, name, audit);
  1099. return setupDone.then(function (chunkLen) {
  1100. if (typeof chunkLen !== "number") {
  1101. chunkLen = 1;
  1102. }
  1103. // Nasty hack to allow us to auto-skip tests where we're actually testing
  1104. // a key-sequence (i.e. targeting terminal.js) when there is no terminal
  1105. if (chunkLen === -1) {
  1106. assert.log("Skipped " + name + " " + skipReason);
  1107. return Promise.resolve(undefined);
  1108. }
  1109. if (assert.currentTest) {
  1110. var responseTime = (new Date().getTime() - start) / chunkLen;
  1111. totalResponseTime += responseTime;
  1112. if (responseTime > maxResponseTime) {
  1113. maxResponseTime = responseTime;
  1114. maxResponseCulprit = assert.currentTest + "/" + name;
  1115. }
  1116. averageOver++;
  1117. }
  1118. var checkDone = helpers._check(options, name, audit.check);
  1119. return checkDone.then(function () {
  1120. var execDone = helpers._exec(options, name, audit.exec);
  1121. return execDone.then(function (data) {
  1122. return helpers._post(name, audit, data).then(function () {
  1123. if (assert.testLogging) {
  1124. log("- END '" + name + "' in " + assert.currentTest);
  1125. }
  1126. });
  1127. });
  1128. });
  1129. });
  1130. }).then(function () {
  1131. return options.automator.setInput("");
  1132. }, function (ex) {
  1133. options.automator.setInput("");
  1134. throw ex;
  1135. });
  1136. };
  1137. /**
  1138. * Compare 2 arrays.
  1139. */
  1140. helpers.arrayIs = function (actual, expected, message) {
  1141. assert.ok(Array.isArray(actual), "actual is not an array: " + message);
  1142. assert.ok(Array.isArray(expected), "expected is not an array: " + message);
  1143. if (!Array.isArray(actual) || !Array.isArray(expected)) {
  1144. return;
  1145. }
  1146. assert.is(actual.length, expected.length, "array length: " + message);
  1147. for (var i = 0; i < actual.length && i < expected.length; i++) {
  1148. assert.is(actual[i], expected[i], "member[" + i + "]: " + message);
  1149. }
  1150. };
  1151. /**
  1152. * A quick helper to log to the correct place
  1153. */
  1154. function log(message) {
  1155. if (typeof info === "function") {
  1156. info(message);
  1157. }
  1158. else {
  1159. console.log(message);
  1160. }
  1161. }
  1162. return { helpers: helpers, assert: assert };
  1163. })();