Skip to content
Browse files

Initial Commit

  • Loading branch information...
0 parents commit 13e4597ef430704fac3bc8312027604bfd75859a Pranav Verma committed Aug 21, 2012
Showing with 11,842 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +7 −0 .npmignore
  3. +75 −0 README.md
  4. +141 −0 arrow_selenium/selenium.js
  5. +29 −0 arrow_server/ghostdriverlauncher.js
  6. +588 −0 arrow_server/server.js
  7. +27 −0 config/config.js
  8. +34 −0 config/descriptor-schema.json
  9. +20 −0 config/dimensions.json
  10. +19 −0 config/qunit-config.js
  11. +1 −0 ghostdriver
  12. +161 −0 index.js
  13. BIN lib/.DS_Store
  14. +327 −0 lib/client/driver.html
  15. +107 −0 lib/client/qunit-runner.js
  16. +15 −0 lib/client/qunit-seed.js
  17. +13 −0 lib/client/qunitHost.html
  18. +14 −0 lib/client/testHost.html
  19. +27 −0 lib/client/yuitest-console.js
  20. +35 −0 lib/client/yuitest-reporter.js
  21. +143 −0 lib/client/yuitest-runner.js
  22. +142 −0 lib/client/yuitest-seed.js
  23. +17 −0 lib/common/yui-arrow.js
  24. +145 −0 lib/controller/default.js
  25. +79 −0 lib/controller/locator.js
  26. +128 −0 lib/driver/node.js
  27. +478 −0 lib/driver/selenium.js
  28. +92 −0 lib/interface/arrow.js
  29. +31 −0 lib/interface/controller.js
  30. +124 −0 lib/interface/driver.js
  31. +352 −0 lib/session/sessionfactory.js
  32. +101 −0 lib/session/testsession.js
  33. +62 −0 lib/session/wdsession.js
  34. +192 −0 lib/util/arrowrecursive.js
  35. +157 −0 lib/util/arrowsetup.js
  36. +35 −0 lib/util/capabilitymanager.js
  37. +73 −0 lib/util/dataprovider.js
  38. +84 −0 lib/util/libmanager.js
  39. +54 −0 lib/util/properties.js
  40. +240 −0 lib/util/reportmanager.js
  41. +70 −0 lib/util/reportstack.js
  42. +4,375 −0 lib/util/webdriver.js
  43. +470 −0 lib/util/ycb.js
  44. +87 −0 nodejs/node.js
  45. +44 −0 package.json
  46. +120 −0 tests/unit/lib/controller/default-tests.js
  47. +68 −0 tests/unit/lib/controller/locator-tests.js
  48. +107 −0 tests/unit/lib/driver/node-tests.js
  49. +297 −0 tests/unit/lib/driver/selenium-tests.js
  50. +67 −0 tests/unit/lib/interface/arrow-tests.js
  51. +279 −0 tests/unit/lib/session/sessionfactory-tests.js
  52. +28 −0 tests/unit/lib/session/testdata/config.js
  53. +36 −0 tests/unit/lib/session/testdata/test-func.js
  54. +78 −0 tests/unit/lib/session/testdata/test-lib.js
  55. +72 −0 tests/unit/lib/session/testdata/testMock.html
  56. +71 −0 tests/unit/lib/session/testdata/test_descriptor.json
  57. +129 −0 tests/unit/lib/session/testsession-tests.js
  58. +117 −0 tests/unit/lib/session/wdsession-tests.js
  59. +1 −0 tests/unit/lib/util/badlibs/jsFileBadExtension
  60. +5 −0 tests/unit/lib/util/badlibs/nonJsFileGoodExtension.js
  61. +17 −0 tests/unit/lib/util/capabilities.json
  62. +39 −0 tests/unit/lib/util/capabilitymanager-tests.js
  63. +15 −0 tests/unit/lib/util/config/configoverride.js
  64. +29 −0 tests/unit/lib/util/config/defaultconfig.js
  65. +34 −0 tests/unit/lib/util/config/descriptor-schema.json
  66. +102 −0 tests/unit/lib/util/dataprovider-tests.js
  67. +20 −0 tests/unit/lib/util/dimensions.json
  68. +80 −0 tests/unit/lib/util/libmanager-tests.js
  69. +84 −0 tests/unit/lib/util/properties-tests.js
  70. +38 −0 tests/unit/lib/util/reportmanager-tests.js
  71. +98 −0 tests/unit/lib/util/reportstack-tests.js
  72. +36 −0 tests/unit/lib/util/testDescriptor.json
  73. +17 −0 tests/unit/stub/arrow.js
  74. +18 −0 tests/unit/stub/controller.js
  75. +38 −0 tests/unit/stub/driver.js
  76. +50 −0 tests/unit/stub/process.js
  77. +89 −0 tests/unit/stub/seleniumserver.js
  78. +144 −0 tests/unit/stub/webdriver.js
4 .gitignore
@@ -0,0 +1,4 @@
+*.swp
+.svn
+./node_modules
+./package.json.pl
7 .npmignore
@@ -0,0 +1,7 @@
+.lock-wscript
+.svn/
+.hg/
+.git/
+CVS/
+.DS_Store
+package.json.pl
75 README.md
@@ -0,0 +1,75 @@
+
+#Arrow
+
+
+
+##Overview
+
+Arrow is a test framework designed to promote test-driven JavaScript development. Arrow provides a consistent test creation and execution environment for both Developers and Quality Engineers.
+
+Arrow aims to completely remove the line between development’s Unit tests, and Functional and Integration tests by providing a uniform way to create and execute both.
+
+Arrow itself is a thin, extensible layer that marries JavaScript, NodeJS, PhantomJS and Selenium. Arrow allows you to write tests using YUI-Test and execute those tests using NodeJS, PhantomJS or Selenium. Additionally, Arrow provides a rich mechanism for building, organizing and executing test and test scenarios.
+
+
+##Options
+
+
+**--help** display this help page <br>
+**--version** display installed arrow version<br>
+**--lib** comma separated list of js files needed by the test<br>
+**--page** path to the mock or production html page, for example: http://www.yahoo.com or mock.html<br>
+**--driver** selenium|phantomjs|browser. (default: phantomjs)<br>
+**--browser** firefox|chrome|opera|reuse. Specify browser version with a hypen, ex.: firefox-4.0 or opera-11.0 (default: firefox)<br>
+**--controller** a custom controller javascript file<br>
+**--reuseSession** true/false. Specifies whether to run tests in existing sessions managed by selenium. Visit http://selenuim_host/wd/hub to setup sessions (default: false)<br>
+**--report** true/false. Creates report files in junit and json format, and also prints a consolidated test report summary on console<br>
+**--testName** comma separated list of test names defined in test descriptor. all other tests will be ignored<br>
+**--group** comma separated list of groups defined in test descriptor, all other groups will be ignored<br>
+**--logLevel** DEBUG|INFO|WARN|ERROR|FATAL (default: INFO)<br>
+**--dimension** a custom dimension file for defining ycb contexts<br>
+**--context** name of ycb context<br>
+
+
+
+##Examples
+
+Below are some examples to help you get started.
+
+###Unit test:
+arrow --lib=../src/greeter.js test-unit.js
+
+###Unit test with a mock page:
+arrow --page=testMock.html --lib=./test-lib.js test-unit.js
+
+###Unit test with selenium:
+arrow --page=testMock.html --lib=./test-lib.js --driver=selenium test-unit.js
+
+###Integration test:
+arrow --page=http://www.hostname.com/testpage --lib=./test-lib.js test-int.js
+
+###Integration test:
+arrow --page=http://www.hostname.com/testpage --lib=./test-lib.js --driver=selenium test-int.js
+
+###Custom controller:
+arrow --controller=custom-controller.js --driver=selenium
+
+
+##Arrow Dependencies
+
+**glob** https://github.com/isaacs/node-glob<br>
+**mockery** https://github.com/nathanmacinnes/Mockery<br>
+**nopt** https://github.com/isaacs/nopt<br>
+**colors** https://github.com/Marak/colors.js<br>
+**express** https://github.com/visionmedia/express<br>
+**yui** http://github.com/yui/yui3<br>
+**JSV** http://github.com/garycourt/JSV<br>
+**log4js** https://github.com/nomiddlename/log4js-node<br>
+**clone** https://github.com/pvorb/node-clone<br>
+**useragent** https://github.com/3rd-Eden/useragent<br>
+**ytestrunner** https://github.com/gotwarlost/ytestrunner<br>
+
+Apart from above mentioned npm modules, Arrow also relies on these two projects
+
+**selenium** https://code.google.com/p/selenium/<br>
+**ghostdriver** https://github.com/detro/ghostdriver
141 arrow_selenium/selenium.js
@@ -0,0 +1,141 @@
+#!/usr/bin/env node
+
+/*jslint forin:true sub:true anon:true, sloppy:true, stupid:true nomen:true, node:true continue:true*/
+
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+var fs = require("fs");
+var nopt = require("nopt");
+var log4js = require("log4js");
+
+var Properties = require("../lib/util/properties");
+var ArrowSetup = require('../lib/util/arrowsetup');
+var WdAppClass = require('../lib/util/webdriver');
+var WdSession = require("../lib/session/wdsession");
+
+//getting command line args
+var argv = nopt();
+
+// singleton app to interact with wd sessions
+var wdApp = new WdAppClass();
+
+//setup config
+var prop = new Properties(__dirname + "/../config/config.js", argv.config, argv);
+var config = prop.getAll();
+//console.log(config);
+var logger = log4js.getLogger("selenium");
+
+function listSessions(error, next, arrSessions) {
+ var sessionCaps = [],
+ sessionCount = 0,
+ i,
+ sessionId,
+ webdriver;
+
+ if (0 === arrSessions.length) {
+ next(sessionCaps);
+ }
+
+ function onSessionCap(val) {
+ sessionCaps[val.capabilities.browserName] = val;
+ sessionCount += 1;
+ if (sessionCount === arrSessions.length) {
+ next(sessionCaps);
+ }
+ }
+
+ for (i = 0; i < arrSessions.length; i += 1) {
+ sessionId = arrSessions[i];
+ webdriver = new wdApp.Builder().
+ usingServer(config["seleniumHost"]).
+ usingSession(sessionId).
+ build();
+ webdriver.session_.then(onSessionCap);
+ }
+}
+
+function describeSession(sessionCap) {
+ console.log(sessionCap);
+}
+
+function describeSessions(sessionCaps) {
+ console.log(sessionCaps);
+}
+
+function openBrowser(sessionCaps) {
+ var browsers = argv.open,
+ browserList = browsers.split(","),
+ webdriver,
+ browser,
+ i,
+ val;
+ for (i = 0; i < browserList.length; i += 1) {
+ browser = browserList[i];
+ if (0 === browser.length) { continue; }
+
+ logger.info("Opening browser: " + browser);
+ if (sessionCaps.hasOwnProperty(browser)) {
+ logger.info("Already open, ignored");
+ continue;
+ }
+
+ webdriver = new wdApp.Builder().
+ usingServer(config["seleniumHost"]).
+ withCapabilities({
+ "browserName": browser,
+ "version": "",
+ "platform": "ANY",
+ "javascriptEnabled": true
+ }).build();
+ webdriver.session_.then(describeSession);
+ }
+}
+
+function closeBrowsers(sessionCaps) {
+ var browser,
+ cap,
+ webdriver;
+ for (browser in sessionCaps) {
+ logger.info("Closing browser: " + browser);
+
+ cap = sessionCaps[browser];
+ webdriver = new wdApp.Builder().
+ usingServer(config["seleniumHost"]).
+ usingSession(cap.id).
+ build();
+ webdriver.quit();
+ }
+}
+
+function listHelp() {
+ console.info("\nCommandline Options :" + "\n" +
+ "--list : Lists all selenium browser sessions" + "\n" +
+ "--open : Comma seperated list of browsers to launch" + "\n" +
+ "--close : Close all selenium controller browser sessions" + "\n"
+ );
+}
+
+
+var arrowSetup = new ArrowSetup(config, argv);
+arrowSetup.setuplog4js();
+arrowSetup.setupSeleniumHost();
+logger.info("Selenium host: " + config["seleniumHost"]);
+
+var hub = new WdSession(config);
+
+if (argv.list || argv.ls) {
+ hub.getSessions(describeSessions, listSessions, false);
+} else if (argv.open) {
+ hub.getSessions(openBrowser, listSessions, true);
+} else if (argv.close) {
+ hub.getSessions(closeBrowsers, listSessions, true);
+} else if (argv.help) {
+ listHelp();
+} else {
+ listHelp();
+}
+
29 arrow_server/ghostdriverlauncher.js
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+
+/*jslint forin:true sub:true anon:true sloppy:true stupid:true nomen:true node:true continue:true*/
+
+var childProcess = require("child_process");
+var fs = require("fs");
+var ghostPort = process.argv[2];
+var arrowHost = process.argv[3];
+
+//console.log(ghostPort + ":" + arrowHost );
+
+process.chdir(__dirname + "/../ghostdriver/src");
+var child = childProcess.spawn("phantomjs", ["main.js", ghostPort]);
+var initPhantom = false;
+
+child.stdout.on("data", function (data) {
+ var pjUrl;
+ console.log(data.toString());
+ if (!initPhantom) {
+ //console.log("Writing arrow_phantom_server.status");
+ pjUrl = "http://" + arrowHost + ":" + ghostPort;
+ fs.writeFileSync("/tmp/arrow_phantom_server.status", pjUrl);
+ initPhantom = true;
+ }
+});
+
+child.stderr.on("data", function (data) {
+ console.error(data.toString());
+});
588 arrow_server/server.js
@@ -0,0 +1,588 @@
+#!/usr/bin/env node
+
+/*jslint forin:true sub:true anon:true sloppy:true stupid:true nomen:true node:true continue:true*/
+
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+var fs = require("fs");
+var os = require("os");
+var urlParser = require("url");
+var querystring = require("querystring");
+var path = require("path");
+var nopt = require("nopt");
+var http = require("http");
+var express = require("express");
+var log4js = require("log4js");
+var childProcess = require("child_process");
+
+log4js.setGlobalLogLevel("INFO");
+var logger = log4js.getLogger("ArrowServer");
+
+var debug = false;
+var arrowHost = "localhost";
+var arrowPortMin = 4459;
+var arrowPortMax = 4459;
+var curArrowPort = arrowPortMin;
+var ghostPort = 4460;
+var arrowPort = 0;
+var arrowAddress = "";
+var parsed = nopt();
+
+//help messages
+function showHelp() {
+ console.info("\nOPTIONS :" + "\n" +
+ " --host : (optional) Fully qualified name of host where arrow server is running. (default: localhost)" + "\n" +
+ " --port : (optional) Arrow Server Port. (default: 4459) " + "\n" +
+ " --ghostPort : (optional) GhostDriver Port. (default: 4460) " + "\n\n"
+ );
+
+ console.log("\nEXAMPLES :" + "\n" +
+ " For local usage: " + "\n" +
+ " arrow_server ( Arrow server will start listening to localhost:4459 )" + "\n\n" +
+ " For remote usage: " + "\n" +
+ " arrow_server --host=minuteblue.corp.yahoo.com --port=4800 ( Arrow server will start listening to minuteblue.corp.yahoo.com:4800) " + "\n\n");
+}
+
+if (parsed.help) {
+ showHelp();
+ process.exit(0);
+}
+
+if (parsed["host"]) {
+ arrowHost = parsed["host"];
+}
+
+if (parsed["port"]) {
+ var port = String(parsed["port"]);
+ if (-1 === port.indexOf("-")) {
+ curArrowPort = arrowPortMin = arrowPortMax = parseInt(port, 10);
+ } else {
+ var range = port.split("-");
+ arrowPortMin = parseInt(range[0], 10);
+ arrowPortMax = parseInt(range[1], 10);
+ }
+}
+
+if (parsed["debug"]) {
+ debug = true;
+}
+
+//starting ghostdriver
+
+if (parsed["ghostPort"]) {
+ ghostPort = String(parsed["ghostPort"]);
+}
+var child = childProcess.spawn("node", [__dirname + "/ghostdriverlauncher.js", ghostPort, arrowHost]);
+
+
+child.stdout.on("data", function (data) {
+ console.log(data.toString());
+});
+child.stderr.on("data", function (data) {
+ console.error(data.toString());
+});
+
+var app = express.createServer(
+ express.logger(),
+ express.cookieParser()
+);
+app.use(express.bodyParser());
+
+var mimes = {
+ "css": "text/css",
+ "js": "text/javascript",
+ "htm": "text/html",
+ "html": "text/html",
+ "ico": "image/vnd.microsoft.icon",
+ "jpg": "image/jpeg",
+ "gif": "image/gif",
+ "png": "image/png",
+ "xml": "text/xml"
+};
+
+function serveStatic(pathname, req, res) {
+ fs.readFile(pathname, function (error, content) {
+ var tmp,
+ ext,
+ mime;
+
+ if (error) {
+ res.writeHead(404);
+ res.end("Error loading file " + pathname + ": " + error, "utf-8");
+ } else {
+ tmp = pathname.lastIndexOf(".");
+ ext = pathname.substring((tmp + 1));
+ mime = mimes[ext] || "text/plain";
+
+ res.writeHead(200, {"Content-Type": mime});
+ res.end(content);
+ }
+ });
+}
+
+function tryPort(port) {
+ if (debug) { console.log("Trying port: " + port); }
+
+ try {
+ app.listen(port);
+ arrowPort = port;
+ arrowAddress = "http://" + arrowHost + ":" + port;
+ console.log("Server running at: " + arrowAddress);
+ fs.writeFileSync("/tmp/arrow_server.status", arrowAddress);
+ } catch (ex) {
+ if ("EADDRINUSE" === ex.code) {
+ curArrowPort += 1;
+ if (curArrowPort > arrowPortMax) {
+ console.log("Failed to bind to any port in the range: " + arrowPortMin + " - " + arrowPortMax);
+ } else {
+ tryPort(curArrowPort);
+ }
+ } else {
+ throw ex;
+ }
+ }
+}
+
+function cleanUp() {
+ try {
+ fs.unlinkSync("/tmp/arrow_server.status");
+ fs.unlinkSync("/tmp/arrow_phantom_server.status");
+ } catch (ex) {}
+ child.kill();
+ console.log("Good bye!");
+}
+
+// file server
+app.get("/arrow", function (req, res) {
+ serveStatic(__dirname + "/../lib/client/driver.html", req, res);
+});
+app.get("/arrow/static/*", function (req, res) {
+ serveStatic("/" + req.params[0], req, res);
+});
+
+// selenium ip hookup
+app.get("/arrow/wd/:selPort", function (req, res) {
+ var selUrl = "http://" + req.connection.remoteAddress + ":" + req.params.selPort + "/wd/hub";
+ fs.writeFileSync("/tmp/arrow_sel_server.status", selUrl);
+ res.end("Selenium captured at: " + selUrl, "utf-8");
+});
+
+var sessions = [];
+var wdtasks = [];
+var wdTaskCounter = 0;
+
+// shared functions
+function validateSession(req, res) {
+ var sessionId = req.params.sessionId,
+ body;
+
+ if (sessions.hasOwnProperty(sessionId)) { return true; }
+
+ body = {
+ status: 9,
+ value: "No such sessionId: " + sessionId
+ };
+ res.send(body, 404);
+ return false;
+}
+
+function queueWdTask(params, req, res) {
+ var sessionId = req.params.sessionId,
+ curTask,
+ body,
+ task,
+ conn,
+ session;
+
+ if (wdtasks.hasOwnProperty(sessionId)) {
+ curTask = wdtasks[sessionId];
+ if (curTask) {
+ body = {
+ status: 9,
+ value: "A command is still running for sessionId: " + sessionId
+ };
+ res.send(body, 500);
+ return false;
+ }
+ }
+
+ task = {
+ "id": "taskid-" + wdTaskCounter,
+ "params": params,
+ "statusCode": 0,
+ "httpCode": 200
+ };
+ wdtasks[sessionId] = {
+ "request": req,
+ "response": res,
+ "task": task
+ };
+ wdTaskCounter += 1;
+
+ session = sessions[sessionId];
+ session.response.send(task);
+ delete sessions[sessionId]; // we dont need this anymore
+
+ conn = req.connection;
+ conn.on("close", function () {
+ if (wdtasks.hasOwnProperty(sessionId)) {
+ if (debug) {
+ console.log("Task connection closed, deleting task for session: " + sessionId);
+ }
+ delete wdtasks[sessionId];
+ }
+ });
+
+ return true;
+}
+
+// browsers running webdriver sessions
+app.post("/arrow/slave/:sessionId", function (req, res) {
+ var timestamp = (new Date()).getTime(),
+ sessionId = req.params.sessionId,
+ prevTask = req.body,
+ prevTaskInfo,
+ resBody,
+ oldSession,
+ conn;
+
+ if (debug) {
+ console.log("Agent response:");
+ console.log(req.body);
+ }
+ prevTask = req.body;
+
+ // a task is also completed, send it to wd client
+ // TODO: validate that the task id matches
+ if (prevTask && prevTask.id && (wdtasks.hasOwnProperty(sessionId))) {
+ prevTaskInfo = wdtasks[sessionId];
+ delete wdtasks[sessionId];
+
+ resBody = {
+ "status": prevTask.statusCode,
+ "sessionId": sessionId,
+ "value": prevTask.result
+ };
+ if (debug) {
+ console.log("Task result:");
+ console.log(resBody);
+ }
+ prevTaskInfo.response.send(resBody, prevTask.httpCode);
+ }
+
+ if (sessions.hasOwnProperty(sessionId)) {
+ if (debug) { console.log("Killing old connection for session: " + sessionId); }
+ oldSession = sessions[sessionId];
+ oldSession.response.end();
+ delete sessions[sessionId];
+ }
+
+ console.log("Session registered: " + sessionId);
+ sessions[sessionId] = {
+ "sessionId": sessionId,
+ "timestamp": timestamp,
+ "request": req,
+ "response": res
+ };
+
+ conn = req.connection;
+ conn.on("close", function () {
+ if (sessions.hasOwnProperty(sessionId)) {
+ oldSession = sessions[sessionId];
+ if (oldSession.timestamp === timestamp) {
+ if (debug) { console.log("Session deleted: " + sessionId); }
+ delete sessions[sessionId];
+ } else {
+ if (debug) { console.log("Session already recaptured: " + sessionId); }
+ }
+ }
+ });
+});
+
+// webdriver
+// Query the server status
+app.get("/wd/hub/status", function (req, res) {
+ var body;
+
+ res.contentType("application/json");
+ body = {
+ build: { version: "1.0" },
+ os: { name: "rhel" }
+ };
+ res.send(body);
+});
+
+// Create a new session
+app.post("/wd/hub/session", function (req, res) {
+ res.contentType("application/json");
+ res.send({status: 9, value: "Create session: Not Implemented"}, 501);
+});
+
+// Get all sessions
+app.get("/wd/hub/sessions", function (req, res) {
+ var sessionIds = [],
+ sessionId,
+ body;
+
+ res.contentType("application/json");
+
+ sessionIds = [];
+ for (sessionId in sessions) {
+ sessionIds.push({"id": sessionId});
+ }
+
+ body = {
+ status: 0,
+ value: sessionIds
+ };
+ res.send(body);
+});
+
+// Retrieve the capabilities of the specified session
+app.get("/wd/hub/session/:sessionId", function (req, res) {
+ var body,
+ session;
+
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ session = sessions[req.params.sessionId];
+ body = {
+ status: 0,
+ sessionId: req.params.sessionId,
+ value: {
+ platform: "ANY",
+ cssSelectorsEnabled: true,
+ javascriptEnabled: true,
+ browserName: session.request.headers["user-agent"],
+ nativeEvents: true,
+ takesScreenshot: false,
+ version: 1
+ }
+ };
+
+ res.send(body);
+});
+
+// Delete the session
+app.del("/wd/hub/session/:sessionId", function (req, res) {
+ res.contentType("application/json");
+ res.send({status: 9, value: "Delete session: Not Implemented"}, 501);
+});
+
+// Get the current page title
+app.get("/wd/hub/session/:sessionId/title", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "title"}, req, res);
+});
+
+// Get the current page url
+app.get("/wd/hub/session/:sessionId/url", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "url"}, req, res);
+});
+
+var revProxyHost = "";
+var revProxyPort = 80;
+// Navigate to the url
+app.post("/wd/hub/session/:sessionId/url", function (req, res) {
+ var url,
+ reqParams,
+ reqHost,
+ reqPort;
+
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+ url = req.body.url;
+
+ reqParams = urlParser.parse(url);
+ reqHost = reqParams.hostname;
+ reqPort = 80;
+ if (reqParams.port) { reqPort = reqParams.port; }
+ if ((reqHost === arrowHost) && (reqPort === arrowPort)) {
+ if (debug) { console.log("Reverse proxy disabled"); }
+ revProxyHost = "";
+ } else {
+ console.log("Reverse proxy enabled for: " + reqHost + ":" + reqPort);
+ revProxyHost = reqHost;
+ revProxyPort = reqPort;
+ url = arrowAddress + reqParams.pathname;
+ }
+
+ queueWdTask({"type": "navigate", "url": url}, req, res);
+});
+
+// Execute sync script
+app.post("/wd/hub/session/:sessionId/execute", function (req, res) {
+ var script;
+
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ script = req.body.script;
+ queueWdTask({"type": "execute", "script": script}, req, res);
+});
+
+// Execute async script
+app.post("/wd/hub/session/:sessionId/execute_async", function (req, res) {
+ res.contentType("application/json");
+ res.send({status: 9, value: "execute_async: Not Implemented"}, 501);
+});
+
+// find an element
+app.post("/wd/hub/session/:sessionId/element", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "element", "using": req.body.using, "value": req.body.value}, req, res);
+});
+
+// find an element starting from
+app.post("/wd/hub/session/:sessionId/element/:id/element", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "element", "element": req.params.id, "using": req.body.using, "value": req.body.value}, req, res);
+});
+
+// find elements
+app.post("/wd/hub/session/:sessionId/elements", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "elements", "using": req.body.using, "value": req.body.value}, req, res);
+});
+
+// find elements starting from
+app.post("/wd/hub/session/:sessionId/elements/:id/element", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "elements", "element": req.params.id, "using": req.body.using, "value": req.body.value}, req, res);
+});
+
+// get text of an element
+app.get("/wd/hub/session/:sessionId/element/:id/text", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "text", "element": req.params.id}, req, res);
+});
+
+// get tag of an element
+app.get("/wd/hub/session/:sessionId/element/:id/name", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "name", "element": req.params.id}, req, res);
+});
+
+// get attribute of an element
+app.get("/wd/hub/session/:sessionId/element/:id/attribute/:name", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "attribute", "element": req.params.id, "name": req.params.name}, req, res);
+});
+
+// click on an element
+app.post("/wd/hub/session/:sessionId/element/:id/click", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "click", "element": req.params.id}, req, res);
+});
+
+// submit a form
+app.post("/wd/hub/session/:sessionId/element/:id/submit", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "submit", "element": req.params.id}, req, res);
+});
+
+// send key strokes to an element
+app.post("/wd/hub/session/:sessionId/element/:id/value", function (req, res) {
+ res.contentType("application/json");
+ if (!validateSession(req, res)) { return; }
+
+ queueWdTask({"type": "value", "element": req.params.id, "value": req.body.value}, req, res);
+});
+
+function serveRevProxy(req, res) {
+ var options,
+ proxy_request;
+
+ if (debug) {
+ console.log("Reverse proxy is serving: http://" + revProxyHost + ":" + revProxyPort + req.url);
+ }
+
+ req.headers["X-Forwarded-For"] = req.connection.remoteAddress;
+ req.headers["Host"] = revProxyHost;
+ options = {
+ host: revProxyHost,
+ port: revProxyPort,
+ path: req.url,
+ method: req.method,
+ headers: req.headers
+ };
+ proxy_request = http.request(options, function (proxy_response) {
+ //send headers and data as received
+ res.writeHead(proxy_response.statusCode, proxy_response.headers);
+ proxy_response.addListener("data", function (chunk) {
+ res.write(chunk);
+ });
+ proxy_response.addListener("end", function () {
+ res.end();
+ });
+ });
+ //deal with errors, timeout, con refused, ...
+ proxy_request.on("error", function (err) {
+ res.writeHead(500);
+ res.end(err.toString(), "utf-8");
+ });
+
+ //proxies to SEND request to the real server
+ req.addListener("data", function (chunk) {
+ proxy_request.write(chunk);
+ });
+ req.addListener("end", function () {
+ proxy_request.end();
+ });
+}
+
+// default handling
+app.get("*", function (req, res) {
+ var docRoot;
+
+ if (revProxyHost.length > 0) {
+ serveRevProxy(req, res);
+ } else {
+ docRoot = process.cwd();
+ if ("/" === docRoot) { docRoot = ""; }
+ serveStatic(docRoot + req.url, req, res);
+ }
+});
+
+
+
+tryPort(curArrowPort);
+process.on("uncaughtException", function (err) {
+ console.log("Uncaught exception: " + err);
+ process.exit();
+});
+process.on("SIGINT", function () {
+ process.exit();
+});
+process.on("exit", function (err) {
+ cleanUp();
+});
+
27 config/config.js
@@ -0,0 +1,27 @@
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+var config = {};
+
+// User default config
+config.seleniumHost = "";//"http://selgrid3.global.media.corp.yahoo.com:80";
+config.context = "";
+config.defaultAppHost = "";
+config.logLevel = "INFO";
+config.browser = "firefox";
+config.parallel = false;
+config.baseUrl = "";
+// Framework config
+config.arrowModuleRoot = global.appRoot + "/";
+config.dimensions = config.arrowModuleRoot + "config/dimensions.json";
+config.defaultTestHost = config.arrowModuleRoot + "lib/client/testHost.html";
+config.defaultAppSeed = "http://yui.yahooapis.com/3.4.1/build/yui/yui-min.js";
+config.testSeed = config.arrowModuleRoot + "lib/client/yuitest-seed.js";
+config.testRunner = config.arrowModuleRoot + "lib/client/yuitest-runner.js";
+config.autolib = config.arrowModuleRoot + "lib/common";
+config.descriptorName = "test_descriptor.json";
+
+module.exports = config;
34 config/descriptor-schema.json
@@ -0,0 +1,34 @@
+{
+ "name" : "test",
+ "type" : "object",
+ "properties" : {
+ "name" : { "type" : "string", "required" :true },
+ "commonlib" : { "type" : "string"},
+ "config" : {
+ "type" : "object"
+ },
+ "dataprovider" : {
+ "type":"object",
+ "required":true,
+ "additionalProperties" : {
+ "type" : "object",
+ "properties" : {
+ "enabled" : { "type" : "boolean" },
+ "controller" : { "type" : "string" },
+ "group" : { "type" : "string" },
+ "browser" : { "type" : "string" },
+ "params" : {"type" : "object",
+ "properties" : {
+ "page" : { "type" : "string" },
+ "test" : { "type" : "string" },
+ "lib" : { "type" : "string" }
+ }
+
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "additionalProperties": false
+}
20 config/dimensions.json
@@ -0,0 +1,20 @@
+[
+ {
+ "dimensions": [
+ {
+ "environment": {
+ "development": {
+ "dev": null,
+ "test": null
+ },
+ "production": {
+ "int": null,
+ "stage": null,
+ "prod": null
+ }
+ }
+ }
+ ]
+ }
+]
+
19 config/qunit-config.js
@@ -0,0 +1,19 @@
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+var config = {};
+
+config.logLevel="$(logLevel)";
+config.seleniumHost="$(seleniumHost)";
+config.browser="$(browser)";
+config.defaultTestHost="/home/y/share/node/arrow/src/client/qunitHost.html";
+
+config.testSeed="/home/y/share/node/arrow/src/client/qunit-seed.js"
+config.testRunner="/home/y/share/node/arrow/src/client/qunit-runner.js"
+config.seleniumServerPath="/Users/vpranav/Programs/selenium-server.jar";
+config.phantomPath="arrow_phantom";
+
+module.exports = config;
1 ghostdriver
@@ -0,0 +1 @@
+Subproject commit a658d9652d29451bc9e0b01a36c0db22b70a9ef5
161 index.js
@@ -0,0 +1,161 @@
+#!/usr/bin/env node
+
+/*jslint forin:true sub:true anon:true, sloppy:true, stupid:true nomen:true, node:true continue:true*/
+
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+var Arrow = require("./lib/interface/arrow");
+var ArrowSetup = require('./lib/util/arrowsetup');
+var nopt = require("nopt");
+var Properties = require("./lib/util/properties");
+var fs = require("fs");
+
+//setting appRoot
+global.appRoot = __dirname;
+
+//recording currentFilder
+global.workingDirectory = process.cwd();
+
+//getting command line args
+
+var knownOpts = {
+ "browser": [String, null],
+ "lib": [String, null],
+ "page": [String, null],
+ "driver": [String, null],
+ "controller": [String, null],
+ "reuseSession": Boolean,
+ "parallel": [Number, null],
+ "report": Boolean,
+ "reportFolder": [String, null],
+ "testName": [String, null],
+ "group": [String, null],
+ "logLevel": [String, null],
+ "context": [String, null],
+ "dimensions": [String, null],
+ "capabilities": [String, null],
+ "seleniumHost": [String, null]
+ },
+ shortHands = {},
+ //TODO : Investigate and implement shorthands
+// , "br" : ["--browser"]
+// , "lb" : ["--lib"]
+// , "p" : ["--page"]
+// , "d" : ["--driver"]
+// , "ct" : ["--controller"]
+// , "rs" : ["--reuseSession"]
+// , "rp" : ["--report"]
+// , "t" : ["--testName"]
+// , "g" : ["--group"]
+// , "ll" : ["--logLevel"]
+// , "cx" : ["--context"]
+// , "dm" : ["--dimension"]
+// , "sh" : ["--seleniumHost"]
+//}
+
+ argv = nopt(knownOpts, shortHands, process.argv, 2),
+ arrow,
+ prop,
+ config,
+ arrowSetup;
+
+//help messages
+function showHelp() {
+ console.info("\nOPTIONS :" + "\n" +
+ " --lib : a comma seperated list of js files needed by the test" + "\n\n" +
+ " --page : (optional) path to the mock or production html page" + "\n" +
+ " example: http://www.yahoo.com or mock.html" + "\n\n" +
+ " --driver : (optional) one of selenium|browser. (default: selenium)" + "\n\n" +
+ " --browser : (optional) a comma seperated list of browser names, optionally with a hypenated version number.\n" +
+ " Example : 'firefox-12.0,chrome-10.0' or 'firefox,chrome' or 'firefox'. (default: firefox)" + "\n\n" +
+ " --controller : (optional) a custom controller javascript file" + "\n\n" +
+ " --reuseSession : (optional) true/false. Determines whether selenium tests reuse existing sessions. (default: false)\n" +
+ " Visit http://<your_selenuim_host>/wd/hub to setup sessions." + "\n\n" +
+ " --parallel : (optional) test thread count. Determines how many tests to run in parallel for current session. (default: 1)\n" +
+ " Example : --parallel=3 , will run three tests in parallel" + "\n\n" +
+ " --report : (optional) true/false. creates report files in junit and json format. (default: true)" + "\n" +
+ " also prints a consolidated test report summary on console. " + "\n\n" +
+ " --reportFolder : (optional) folderPath. creates report files in that folder. (default: descriptor folder path)" + "\n\n" +
+ " --testName : (optional) comma seprated list of test name(s) defined in test descriptor" + "\n" +
+ " all other tests will be ignored." + "\n\n" +
+ " --group : (optional) comma seprated list of group(s) defined in test descriptor." + "\n" +
+ " all other groups will be ignored." + "\n\n" +
+ " --logLevel : (optional) one of DEBUG|INFO|WARN|ERROR|FATAL. (default: INFO)" + "\n\n" +
+ " --dimensions : (optional) a custom dimension file for defining ycb contexts" + "\n\n" +
+ " --context : (optional) name of ycb context" + "\n\n" +
+ " --seleniumHost : (optional) override selenium host url (example: --seleniumHost=http://host.com:port/wd/hub)" + "\n\n" +
+ " --capabilities : (optional) the name of a json file containing webdriver capabilities required by your project" +
+ " \n\n");
+
+ console.log("\nEXAMPLES :" + "\n" +
+ " Unit test: " + "\n" +
+ " arrow test-unit.js --lib=../src/greeter.js" + "\n\n" +
+ " Unit test with a mock page: " + "\n" +
+ " arrow test-unit.js --page=testMock.html --lib=./test-lib.js" + "\n\n" +
+ " Unit test with selenium: \n" +
+ " arrow test-unit.js --page=testMock.html --lib=./test-lib.js --driver=selenium" + "\n\n" +
+ " Integration test: " + "\n" +
+ " arrow test-int.js --page=http://www.hostname.com/testpage --lib=./test-lib.js" + "\n\n" +
+ " Integration test: " + "\n" +
+ " arrow test-int.js --page=http://www.hostname.com/testpage --lib=./test-lib.js --driver=selenium" + "\n\n" +
+ " Custom controller: " + "\n" +
+ " arrow --controller=custom-controller.js --driver=selenium");
+}
+
+if (argv.help) {
+ showHelp();
+ process.exit(0);
+}
+
+if (argv.version) {
+ console.log("v" + JSON.parse(fs.readFileSync(global.appRoot + "/package.json", "utf-8")).version);
+ process.exit(0);
+}
+
+//store start time
+global.startTime = Date.now();
+
+//check if user wants to override default config.
+if (!argv.config) {
+ try {
+ if (fs.lstatSync("config.js").isFile()) {
+ argv.config = process.cwd() + "/config.js";
+ }
+ } catch (e) {
+ //console.log("No Custom Config File.")
+ }
+}
+
+//setup config
+prop = new Properties(__dirname + "/config/config.js", argv.config, argv);
+config = prop.getAll();
+
+//expose classes for test/external usage
+this.controller = require('./lib/interface/controller');
+this.log4js = require('log4js');
+
+// TODO: arrowSetup move to Arrow
+arrowSetup = new ArrowSetup(config, argv);
+this.arrow = Arrow;
+
+// Setup Arrow Tests
+if (argv.arrowChildProcess) {
+ //console.log("Child Process");
+ arrowSetup.childSetup();
+ argv.descriptor = argv.argv.remain[0];
+ arrow = new Arrow(config, argv);
+ arrow.run();
+} else {
+ //console.log("Master Process");
+ arrowSetup.setup();
+ if (false === arrowSetup.startRecursiveProcess) {
+ arrow = new Arrow(config, argv);
+ arrow.run();
+ }
+}
+
+
BIN lib/.DS_Store
Binary file not shown.
327 lib/client/driver.html
@@ -0,0 +1,327 @@
+
+<html>
+<head>
+ <title>Browser driver</title>
+ <script src="../../node_modules/yui/yui/yui-min.js"></script>
+
+<script>
+
+function startAgent() {
+ var CONTENTWINDOW = "contentWindow",
+ testReport = null,
+ elementCache = {},
+ autw = getFrame();
+
+ var date = new Date();
+ var sessionId = "session-" + date.getTime();
+ var Y = YUI();
+ Y.use("node", "event", "node-event-simulate", "io", "json-parse", "json-stringify", function() {
+ window.setTimeout(queryTask, 0);
+ });
+
+var task = null;
+
+function queryTask() {
+
+ if(task) delete task.params; // avoid posting back scripts etc to reduce payload
+ else task = {};
+
+ Y.io("/arrow/slave/" + sessionId, {
+ method: "POST",
+ headers: {"Connection": "keep-alive", "Content-Type": "application/json; charset=utf-8"},
+ data: Y.JSON.stringify(task),
+ on: {
+ success: function(id, o) {
+ task = Y.JSON.parse(o.responseText);
+ if(!task || !task.id) {
+ task = null;
+ window.setTimeout(queryTask, 0);
+ } else {
+ window.setTimeout(runTask, 0);
+ }
+ },
+ failure: function(id, o) {
+ window.setTimeout(queryTask, 5000);
+ }
+ }
+ });
+
+ task = null;
+}
+
+function runTask() {
+ var taskDone = true;
+try {
+
+ switch(task.params.type) {
+ case "title": {
+ task.result = (autw.document ? autw.document.title : "");
+ }
+ break;
+ case "url": {
+ task.result = autw.location.href;
+ }
+ break;
+ case "navigate": {
+ navigate(autw, task.params.url);
+ taskDone = false;
+ }
+ break;
+ case "execute": {
+ executeScript();
+ }
+ break;
+ case "element": {
+ findElement(false);
+ }
+ break;
+ case "elements": {
+ findElement(true);
+ }
+ break;
+ case "text": {
+ var autElement = getAUTElement(task.params.element);
+ if(autElement) task.result = autElement.get("text");
+ }
+ break;
+ case "name": {
+ var autElement = getAUTElement(task.params.element);
+ if(autElement) task.result = autElement.get("nodeName");
+ }
+ break;
+ case "attribute": {
+ var autElement = getAUTElement(task.params.element);
+ if(autElement) {
+ task.result = autElement.get(task.params.name);
+ }
+ }
+ break;
+ case "click": {
+ var autElement = getAUTElement(task.params.element);
+ if(autElement) {
+ autElement.simulate("click");
+ }
+ }
+ break;
+ case "submit": {
+ var autElement = getAUTElement(task.params.element);
+ if(autElement) {
+ autElement.submit();
+ }
+ }
+ break;
+ case "value": {
+ var autElement = getAUTElement(task.params.element);
+ if(autElement) {
+ autElement.set("value", task.params.value);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+} catch(ex) {
+ console.log(ex);
+ task.result = { "message": ex };
+ task.statusCode = 13; // UnknownError
+ task.httpCode = 500;
+}
+
+ if(taskDone) {
+ queryTask();
+ }
+}
+
+function executeScript() {
+ try {
+ var tmpFunction = new autw['Function'](task.params.script);
+ task.result = tmpFunction.apply(null);
+ } catch(ex) {
+ console.log("Failed to execute script: " + task.params.script);
+ console.log(ex);
+ task.result = "Failed to execute injected script";
+ task.statusCode = 17; // JavaScriptError
+ }
+}
+
+function findElement(all) {
+ var start = task.params.element;
+ var startNode = null;
+ if(start) {
+ var startNode = getAUTElement(start);
+ if(!startNode) return;
+ } else {
+ startNode = Y.one(autw.document.body);
+ }
+
+ var strategy = task.params.using;
+ var value = task.params.value;
+ var findFunction = all ? findAllElements : findSingleElement;
+
+ switch(strategy) {
+ case "id": {
+ findFunction(startNode, "#" + value);
+ }
+ break;
+ case "name": {
+ findFunction(startNode, "[name=" + value + "]");
+ }
+ break;
+ case "class name": {
+ findFunction(startNode, "." + value + (all ? "" : ":first-child"));
+ }
+ break;
+ case "css selector": {
+ findFunction(startNode, value);
+ }
+ break;
+ case "tag name": {
+ findFunction(startNode, value + (all ? "" : ":first-child"));
+ }
+ break;
+ case "link text": {
+ var resultNodes = [];
+ var nodes = startNode.all("a");
+ for(var i = 0; i < nodes.size(); i++) {
+ var node = nodes.item(i);
+ if(value == node.get("text")) {
+ resultNodes.push(node);
+ if(!all) break;
+ }
+ }
+
+ if(all) {
+ findAUTElements(resultNodes);
+ } else {
+ var resultNode = (resultNodes.length ? resultNodes[0] : null);
+ findAUTElement(resultNode, "link text - " + value);
+ }
+ }
+ break;
+ case "partial link text": {
+ var resultNodes = [];
+ var nodes = startNode.all("a");
+ for(var i = 0; i < nodes.size(); i++) {
+ var node = nodes.item(i);
+ if(-1 != node.get("text").indexOf(value)) {
+ resultNodes.push(node);
+ if(!all) break;
+ }
+ }
+
+ if(all) {
+ findAUTElements(resultNodes);
+ } else {
+ var resultNode = (resultNodes.length ? resultNodes[0] : null);
+ findAUTElement(resultNode, "partial link text - " + value);
+ }
+ }
+ break;
+ case "xpath":
+ // Do we need this?
+ default: {
+ task.statusCode = 9;
+ task.result = "Find element using " + strategy + ": Not Implemented";
+ task.httpCode = 501;
+ }
+ break;
+ }
+}
+
+function findSingleElement(startNode, selector) {
+ var node = startNode.one(selector);
+ findAUTElement(node);
+}
+
+function findAllElements(startNode, selector) {
+ var nodes = startNode.all(selector);
+ findAUTElement(nodes);
+}
+
+function findAUTElement(node, selector) {
+ if(null == node) {
+ task.statusCode = 7;
+ task.result = "Failed to find element using selector: " + selector;
+ } else {
+ var eid = getAUTElementId(node);
+ task.result = {
+ "ELEMENT": eid
+ }
+ }
+}
+
+function findAUTElements(nodes) {
+ var result = [];
+ for(var i = 0; i < nodes.length; i++) {
+ var node = nodes[i];
+ var eid = getAUTElementId(node);
+ result.push({ "ELEMENT": eid });
+ }
+ task.result = result;
+}
+
+var autElementCache = {};
+var autElementId = 0;
+function onAUTLoaded() {
+ console.log("aut: " + autw.location.href);
+
+ // clear out element cache, as we are on to a new page
+ autElementCache = {};
+ autElementId = 0;
+
+ if(!task || !task.params || ("navigate" != task.params.type)) {
+ return;
+ }
+
+ task.result = autw.location.href;
+ queryTask();
+}
+window.onContainerLoaded = onAUTLoaded;
+
+function getAUTElement(elemId) {
+ if(elemId in autElementCache) {
+ return autElementCache[elemId];
+ } else {
+ task.result = "";
+ task.statusCode = 10; // StaleElementReference
+ return null;
+ }
+}
+
+function getAUTElementId(node) {
+ var eid = "eid-" + autElementId++;
+ autElementCache[eid] = node;
+ return eid;
+}
+
+function navigate(frame, url) {
+ frame.location.href = url;
+}
+
+// caching getElementById
+function _ (id) {
+ if (!(id in elementCache)) elementCache[id] = document.getElementById(id);
+ return elementCache[id];
+}
+
+function getFrame() {
+ var frame = _("aut");
+ return frame[CONTENTWINDOW] || frame.contentDocument[CONTENTWINDOW];
+}
+
+}
+
+// Will get replaced once agent starts
+function onContainerLoaded() {}
+</script>
+</head>
+
+<body>
+<div id="bd" width="100%" height="100%">
+ <iframe id="aut" onload="onContainerLoaded()" src="about:blank" frameBorder="1" style="display:block;width:100%;height:100%"></iframe>
+</div>
+<script>
+ startAgent();
+</script>
+</body>
+</html>
107 lib/client/qunit-runner.js
@@ -0,0 +1,107 @@
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+window.qunitReport = null;
+window.arrowGetTestReport = function() {
+ return window.qunitReport;
+}
+
+window.setTimeout(function () {
+ var current_test_assertions = [];
+ var module = "default";
+ var report = {
+ "passed": 0,
+ "failed": 0,
+ "total": 0,
+ "type": "report",
+ "name": "QUnit Test Results",
+ "default": {
+ "passed": 0,
+ "failed": 0,
+ "total": 0,
+ "type": "testsuite",
+ "name": "default"
+ }
+ };
+
+ QUnit.moduleStart(function(context) {
+ module = context.name;
+ report[module] = {
+ "passed": 0,
+ "failed": 0,
+ "total": 0,
+ "type": "testsuite",
+ "name": module
+ };
+ });
+
+ QUnit.testDone(function(result) {
+ var name = module + ': ' + result.name;
+ var i;
+
+ if (result.failed) {
+ console.log('Assertion Failed: ' + name);
+
+ var message = "";
+ for (i = 0; i < current_test_assertions.length; i++) {
+ message += current_test_assertions[i] + " ";
+ }
+ console.log(message);
+
+ report["failed"] += 1;
+ report[module]["failed"] += 1;
+
+ report[module][result.name] = {
+ "result": "fail",
+ "name": result.name,
+ "type": "test",
+ "message": message
+ };
+ } else {
+ report["passed"] += 1;
+ report[module]["passed"] += 1;
+
+ report[module][result.name] = {
+ "result": "pass",
+ "name": result.name,
+ "type": "test",
+ "message": "Test passed."
+ };
+ }
+
+ report["total"] += 1;
+ report[module]["total"] += 1;
+ current_test_assertions = [];
+ });
+
+ QUnit.log(function(details) {
+ var response;
+
+ if (details.result) {
+ return;
+ }
+
+ response = details.message || '';
+
+ if (typeof details.expected !== 'undefined') {
+ if (response) {
+ response += ', ';
+ }
+
+ response += 'expected: ' + details.expected + ', but was: ' + details.actual;
+ }
+
+ current_test_assertions.push('Failed assertion: ' + response);
+ });
+
+ QUnit.done(function(result){
+ console.log('Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.');
+ window.qunitReport = JSON.stringify(report);
+ });
+
+ QUnit.start();
+}, 0);
+
15 lib/client/qunit-seed.js
@@ -0,0 +1,15 @@
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+window.arrowInitStatus = "no";
+window.arrowGetInitStatus = function() {
+ return window.arrowInitStatus;
+}
+
+window.$.getScript("http://code.jquery.com/qunit/qunit-git.js", function() {
+ window.arrowInitStatus = "yes";
+ QUnit.init();
+});
13 lib/client/qunitHost.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Default test host for qunit</title>
+
+ <script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
+</script>
+
+</head>
+
+<body></body>
+</html>
14 lib/client/testHost.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>Default test host</title>
+
+ <!--<script src="yui/3.4.1/yui-event-intl-io-node-test.js"></script>-->
+ <script src="../../node_modules/yui/yui/yui-min.js"></script>
+ <script src="../../node_modules/yui/loader/loader-min.js"></script>
+ <link rel="stylesheet" href="../../node_modules/yui/test/assets/skins/sam/test.css">
+</head>
+
+<body></body>
+</html>
27 lib/client/yuitest-console.js
@@ -0,0 +1,27 @@
+/*jslint forin:true sub:true anon:true, sloppy:true, stupid:true nomen:true, node:true continue:true*/
+/*jslint undef: true*/
+/*
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyrights licensed under the New BSD License.
+ * See the accompanying LICENSE file for terms.
+ */
+
+YUI().use("io", "json-stringify", function (Y) {
+
+ function postConsoleLog(log) {
+ var msg = { "ua" : navigator.userAgent, "message" : log};
+ Y.io("/arrow/event/console", {
+ method : "POST",
+ data : Y.JSON.stringify(msg)
+ });
+ }
+
+ var postConsole = function () {
+ };
+ postConsole.log = postConsoleLog;
+ postConsole.info = postConsoleLog;
+
+ if (window.console) {
+ window.console = postConsole;
+ }
+});
35 lib/client/yuitest-reporter.js
@@ -0,0 +1,35 @@
+/*jslint forin:true sub:true anon:true, sloppy:true, stupid:true nomen:true, node:true continue:true*/
+/*jslint undef: true*/
+/*
+ * Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+ * Copyrights licensed under the New BSD License.
+ * See the accompanying LICENSE file for terms.
+ */
+
+YUI().use("io", "test", function (Y) {
+
+ var retryCount = 0,
+ reportInterval = window.setInterval(function () {
+ console.log("Waiting for the test report");
+ var YTest = window.YUITest,
+ results;
+ if (YTest && YTest.TestRunner._root && YTest.TestRunner._root.results &&
+ YTest.TestRunner._root.results.type === "report") {
+ window.clearInterval(reportInterval);
+
+ results = YTest.TestRunner._root.results;
+ results.ua = navigator.userAgent;
+ Y.io("/arrow/event/report", {
+ method : "POST",
+ data : YTest.TestRunner.getResults(YTest.ResultsFormat.JSON)
+ });
+ } else {
+ retryCount = retryCount + 1;
+ if (retryCount > 10) {
+ window.clearInterval(reportInterval);
+ console.log("Failed to collect the test report");
+ }
+ }
+ }, 500);
+});
+
143 lib/client/yuitest-runner.js
@@ -0,0 +1,143 @@
+/*jslint forin:true sub:true anon:true, sloppy:true, stupid:true nomen:true, node:true continue:true*/
+/*jslint undef: true*/
+
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+YUI({ useBrowserConsole: true }).use("get", function (Y) {
+
+ function loadAction() {
+ YUI({ useBrowserConsole: true }).use(ARROW.testBag, function (Y) {
+ var action = Y.arrow ? Y.arrow.action : null;
+
+ if (!action) {
+ ARROW.actionReport = JSON.stringify({"error": "Could not find an action to execute"});
+ return;
+ }
+
+ try {
+ action.testParams = ARROW.testParams;
+
+ action.setUp(function (error, data) {
+ var interval;
+
+ ARROW.actionReport = JSON.stringify({"error": error, "data": data});
+ if (error) { // we cannot execute the action
+ return;
+ }
+ // wait for the report to be collected before executing the action
+ // because actions are supposed to navigate away
+ interval = setInterval(function () {
+ if (ARROW.actionReported) {
+ clearInterval(interval);
+ action.execute();
+ }
+ }, 100);
+ });
+ } catch (ex) {
+ ARROW.actionReport = JSON.stringify({"error": "Exception: " + ex});
+ }
+ });
+ }
+
+ function completeTest(testRunner) {
+ var YTest = YUITest;
+
+ testRunner = YTest.TestRunner;
+
+ if (typeof navigator !== "undefined") {
+ testRunner._root.results.ua = navigator.userAgent;
+ }
+ ARROW.testReport = testRunner.getResults(YTest.ResultsFormat.JSON);
+ YUITest = undefined;
+ }
+
+ function runTest() {
+ var YTest = YUITest,
+ testRunner = YTest.TestRunner,
+ i;
+
+ function injectConfig(suites) {
+ for (i = 0; i < suites.length; i += 1) {
+ var suite = suites[i];
+ suite.testParams = ARROW.testParams;
+ if (suite.items) {
+ injectConfig(suite.items);
+ }
+ }
+ }
+ injectConfig(testRunner.masterSuite.items);
+
+ testRunner.subscribe(testRunner.COMPLETE_EVENT, completeTest);
+ testRunner.run();
+ }
+
+ function loadTest() {
+ YUI({ useBrowserConsole: true }).use(ARROW.testBag, function (Y) {
+ runTest();
+ });
+ }
+
+ function autoTest() {
+ var YTest = window.YUITest,
+ testRunner;
+ if (!YTest) {
+ return window.setTimeout(autoTest, 50);
+ }
+
+ testRunner = YTest.TestRunner;
+ if (testRunner._root && testRunner._root.results && "report" === testRunner._root.results.type) {
+ completeTest();
+ } else {
+ testRunner.subscribe(testRunner.COMPLETE_EVENT, completeTest);
+ }
+ }
+
+ function fetchAction(actionScript) {
+ if (actionScript.length > 0) {
+ Y.Get.script(actionScript, {
+ onSuccess: function(o) { loadAction(); }
+ });
+ } else {
+ loadAction();
+ }
+ }
+
+ function fetchTest(testScript) {
+ if (testScript.length > 0) {
+ Y.Get.script(testScript, {
+ onSuccess: function (o) { loadTest(); }
+ });
+ } else {
+ loadTest();
+ }
+ }
+
+ function fetchScript() {
+
+ if (ARROW.scriptType) {
+ if ("test" === ARROW.scriptType) {
+ fetchTest(ARROW.testScript);
+ } else {
+ fetchAction(ARROW.actionScript);
+ }
+ } else {
+ fetchTest(ARROW.testScript);
+ }
+ }
+
+ if (ARROW.autoTest) {
+ autoTest();
+ } else if (ARROW.testLibs.length > 0) {
+ Y.Get.script(ARROW.testLibs, {
+ onSuccess: function(o) { fetchScript(); },
+ async: false // TODO: could async true create dependency issue
+ });
+ } else {
+ fetchScript();
+ }
+});
+
142 lib/client/yuitest-seed.js
@@ -0,0 +1,142 @@
+/*jslint forin:true sub:true undef: true anon:true, sloppy:true, stupid:true nomen:true, node:true continue:true*/
+/*jslint undef: true*/
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+// Provided by the fw
+// ARROW = {};
+// ARROW.autoTest = false; // for self contained app and test, such as html
+// ARROW.testParams = {};
+// ARROW.appSeed = ""; // YUI min or equivalent
+// ARROW.testLibs = [];
+// ARROW.scriptType = "test";
+// ARROW.testScript = "test-file.js";
+// ARROW.actionScript = "action-file.js";
+// ARROW.onSeeded = function() { /* add test, hand over to runner */}
+
+ARROW.testBag = ["test"];
+ARROW.testReport = null;
+ARROW.actionReport = null;
+ARROW.actionReported = false;
+
+// try to catch unhandled errors
+if ((typeof window !== "undefined") && !window.onerror) {
+ window.onerror = function (errorMsg, sourceUrl, lineNumber) {
+ console.log("javascript error: " + errorMsg + " at " + lineNumber + ", url: " + sourceUrl);
+ return true;
+ };
+}
+
+(function () {
+
+ function loadScript(url, callback) {
+ var script = document.createElement("script");
+ script.type = "text/javascript";
+
+ if (script.readyState) { // IE
+ script.onreadystatechange = function () {
+ if (("loaded" === script.readyState) || ("complete" === script.readyState)) {
+ script.onreadystatechange = null;
+ callback();
+ }
+ };
+ } else { // Others
+ script.onload = function() {
+ callback();
+ };
+ }
+
+ script.src = url;
+ document.body.appendChild(script);
+ }
+
+ function captureConsoleMessages() {
+
+ try {
+ if(console) {
+ //capturing console log
+ console.oldLog = console.log;
+ console.log = function(line) {
+ ARROW.consoleLog += "[LOG] " + line + "\n";
+ console.oldLog(line);
+ }
+
+ //capturing console info
+ console.oldInfo = console.info;
+ console.info = function(line) {
+ ARROW.consoleLog += "[INFO] " + line + "\n";
+ console.oldInfo(line);
+ }
+
+ //capturing console warn
+ console.oldWarn = console.warn;
+ console.warn = function(line) {
+ ARROW.consoleLog += "[WARN] " + line + "\n";
+ console.oldWarn(line);
+ }
+
+ //capturing console debug
+ console.oldDebug = console.debug;
+ console.debug = function(line) {
+ ARROW.consoleLog += "[DEBUG] " + line + "\n";
+ console.oldDebug(line);
+ }
+
+ //capturing console debug
+ console.oldError = console.error;
+ console.error = function(line) {
+ ARROW.consoleLog += "[ERROR] " + line + "\n";
+ console.oldError(line);
+ }
+ }
+ } catch (e){
+
+ }
+
+ }
+
+ function onYUIAvailable() {
+ var module = ARROW.testParams["module"],
+ yuiAddFunc = YUI.add;
+
+ //initializing Arrow console log
+ ARROW.consoleLog = "";
+
+ //capturing console messages
+ captureConsoleMessages();
+
+ // capture module style tests
+ YUI.add = function (name, fn, version, meta) {
+ yuiAddFunc(name, fn, version, meta);
+
+ if (module && (name !== module)) {
+ return;
+ }
+
+ if (("test" === ARROW.scriptType) && (-1 !== name.indexOf("-tests"))) {
+ console.log("Found test module: " + name);
+ ARROW.testBag.push(name);
+ } else if (("action" === ARROW.scriptType) && (-1 !== name.indexOf("-action"))) {
+ console.log("Found test action: " + name);
+ ARROW.testBag.push(name);
+ }
+ };
+
+ ARROW.onSeeded();
+ }
+
+ if (typeof YUI === "undefined") {
+ if ((typeof process !== "undefined") && (typeof require !== "undefined")) {
+ YUI = require("yui").YUI;
+ onYUIAvailable();
+ } else {
+ loadScript(ARROW.appSeed, onYUIAvailable);
+ }
+ } else {
+ onYUIAvailable();
+ }
+})();
+
17 lib/common/yui-arrow.js
@@ -0,0 +1,17 @@
+//TODO Figure out and add some good common methods for all arrow users
+/*jslint forin:true sub:true undef: true anon:true, sloppy:true, stupid:true nomen:true, node:true continue:true*/
+/*jslint undef: true*/
+// Augment YUI with
+// - expressive asserts
+// - selenium like query
+
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+YUI.add("arrow", function (Y) {
+
+}, "0.1", { requires: ["test"]});
+
145 lib/controller/default.js
@@ -0,0 +1,145 @@
+/*jslint forin:true sub:true anon:true, sloppy:true, stupid:true nomen:true, node:true continue:true*/
+
+/*
+* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
+* Copyrights licensed under the New BSD License.
+* See the accompanying LICENSE file for terms.
+*/
+
+var util = require("util");
+var log4js = require("log4js");
+var Controller = require("../interface/controller");
+var Arrow = require("../interface/arrow");
+
+/**
+ * Default controller example:
+ *
+ * Open page and test
+ * "test_name" : {
+ * "params": {
+ * "page": "http://www.foo.com/app",
+ * "test": "test-func.js",
+ * "lib": "test-lib.js" // optional
+ * }
+ * }
+ *
+ * Scenario
+ * "scenario_test": {
+ * "params": {
+ * "scenario": [ // scenario is a sequence of atoms
+ * {
+ * "page": "http://www.foo.com/app" // this atom uses the default controller
+ * },
+ * {
+ * "controller": "custom_controller",
+ * "params": {
+ * // controller specific parameter
+ * }
+ * },
+ * {
+ * "test": "test-quote.js" // this atom uses the default controller
+ * }
+ * ]
+ * }
+ *
+ * @param testConfig values from the config section in the descriptor ycb
+ * @param testParams values from the params section in the test
+ * @param driver instance
+ *
+ */
+function DefaultController(testConfig, testParams, driver) {
+ Controller.call(this, testConfig, testParams, driver);
+
+ this.logger = log4js.getLogger("DefaultController");
+}
+
+util.inherits(DefaultController, Controller);
+
+/**
+ * Handles scenario execution
+ * @private
+ *
+ * @param callback function to call when done. It receives errorMsg as a parameter.
+ */
+DefaultController.prototype.executeScenario = function (callback) {
+ var self = this,
+ arrow = Arrow.getInstance(),
+ scenario = this.testParams.scenario,
+ index = 0;
+
+ // private function to sequenece atoms inside a scenario
+ function runNextChild() {
+ var child,
+ childParams,
+ controllerName,
+ childLib;
+
+ if (index === scenario.length) {
+ callback();
+ return;
+ }
+
+ child = scenario[index];
+ index += 1;
+
+ // In an atom, "controller" and "params" are required with the exception of the default
+ // controller; in which case all the direct children of the atom are treates as "params".
+ controllerName = child["controller"];
+ childParams = child.params;
+ if (!childParams) {
+ if (!controllerName) {
+ childParams = child;
+ } else {
+ childParams = {};
+ }
+ }
+
+ childLib = childParams.lib;
+ if (childLib) {
+ if (self.testParams.lib) { childLib = self.testParams.lib + "," + childLib; }
+ } else {
+ childLib = self.testParams.lib;
+ }
+ childParams.lib = childLib;
+
+ // TODO: add to the controllers report success or failure
+ arrow.runController(controllerName, self.testConfig, childParams, self.driver, function (error) {
+ if (error) {
+ var errorMsg = "Scenario failed at atom: " + (index - 1);
+ callback(errorMsg);
+ } else {
+ runNextChild();
+ }
+ });
+ }
+
+ // initiate the sequence
+ runNextChild();
+};
+
+/**
+ * Default controller that opens a page and tests. It also handles scenario
+ * execution if given.
+ *
+ * @param callback function to call when done. It receives errorMsg as a parameter.
+ */
+DefaultController.prototype.execute = function (callback) {
+ var self = this;
+
+ if (self.testParams.scenario) {
+ self.executeScenario(callback);
+ } else if (self.testParams.action) {
+ self.driver.executeAction(self.testConfig, self.testParams, callback);
+ } else {
+ if (!self.testParams.test && self.testParams.page) {
+ self.driver.navigate(self.testParams.page, callback);
+ } else if (self.testParams.test) {
+ self.driver.executeTest(self.testConfig, self.testParams, callback);