diff --git a/README.md b/README.md old mode 100644 new mode 100755 index e69de29..771ed94 --- a/README.md +++ b/README.md @@ -0,0 +1,178 @@ +# FORKED FROM + +https://github.com/Lullabot/drupal-boilerplate + +# Automated testing + +This directory contains [CasperJS](http://casperjs.org) tests that ensure the +stability of the site. + +## Installation + +### Requirements + +* Python 2.6 or greater. +* PhantomJS 1.9.2. +* CasperJS 1.1-beta3. + +### OSX - Homebrew +Homebrew will install the phantomjs dependency automatically. + +```bash +brew install casperjs --devel +``` + +### Manual installation + +#### Python + +Python is already installed in most platforms. Check the pyhon version with the +following command: + +```bash +python --version +``` + +If the above output is less than 2.6, go to https://www.python.org/download, +find the link for your platform at the 2.7 section (even though CasperJS seems +to work on Python 3 we have not tested it yet). + +You can find further installation tips at +http://docs.casperjs.org/en/latest/installation.html#prerequisites + +#### CasperJS + +```bash +cd /usr/share +sudo git clone git://github.com/n1k0/casperjs.git +cd casperjs +git checkout 1.1-beta3 +sudo ln -sf `pwd`/bin/casperjs /usr/local/bin/casperjs +``` + +#### PhantomJS + +Download phantomjs 1.9.2 http://phantomjs.org/download.html to `/usr/share` + +```bash +cd /usr/share/phantomjs-1.9.2-linux-x86_64/bin +sudo ln -sf `pwd`/phantomjs /usr/local/bin/phantomjs +``` + +### Verify your installation + +When running `casperjs`, the output should be: + +```bash +casperjs +CasperJS version 1.1.0-beta3 at /usr/share/casperjs, using phantomjs version 1.9.2 +``` + +## Running tests +Assuming your local environment is set up at http://drupal.local, all tests may +be run with the following command: + +```bash +cd path-to-drupal-root +drush casperjs --url=http://drupal.local +``` + +You can also run a specific test by giving it as an argument to the command: + +```bash +drush casperjs --url=http://drupal.local +``` + +*NOTE* `drush casperjs` is a wrapper for `casperjs` which sets some useful defaults +when running tests for your Drupal site. Run `drush casperjs -h` for a list of all the +available options and arguments. + +## Writing tests + +Tests are JavaScript files which are located at the `tests` directory. +They can be organized either by which aspect of the site they test (the homepage, +the contact form, the editorial workflow) or by mapping its name with a module +so it tests the functionality added by that particular module. The choice is +yours to take. + +In order to write a new test, copy any of the existing ones and use it as a +template. Be sure to read through the includes such as `drush/commands/casperjs/includes/common.js` +and drush/commands/casperjs/includes/session.js to find if there are any helper +functions you should be aware of in addition to the ones provided by CasperJS. + +Some useful resources for writing tests are: + * [Navigation steps](http://docs.casperjs.org/en/latest/faq.html#how-does-then-and-the-step-stack-work) + let you wait for certain events such a page getting fully rendered before + running assertions over it. + * The [casper](http://docs.casperjs.org/en/latest/modules/casper.html) object has + commands to interact with the browser such as opening a URL or filling + out a form. + * The [test](http://docs.casperjs.org/en/latest/modules/tester.html) + object contains methods to run assertions over the current context. + * `drush/commands/casperjs/includes/common.js` has a list of useful methods to + navigate through a Drupal project. Rely on these methods as much as possible + in order to simplify your testing code. + * `drush/commands/casperjs/includes/session.js` implements session management so + you can swap from an anonymous user to an authentcated user in just one step. + +## Cookies +Some tests like will need an authenticated user to work with order to test the +backend. The `drush casperjs` command creates a temporary cookies file at the +temorary directory while running tests to store cookie information and deletes it +the next time tests are run. + +If you need session management in your test call `casper.boilerRun()` to run +your test so that all opened sessions are closed for the next test run. + +## Tips +### Taking screenshots +You can take a screenshot and capture markup with `casper.boilerCapture('my-unique-string');`. +This is perfectly fine to commit to your test suite, as this will do nothing +unless you specifically configured your environment to actually save these files. +There are three ways to do this: + +The first is to turn on capturing by exporting +an environment variable: + +```bash +export BOILER_TEST_CAPTURE=true +``` + +The second is to run the `drush casperjs` command with debug mode turned on. +See the *Debugging* section for more details on that. + +The third method is to pass `true` as the second argument to `casper.boilerCapture()`. +This will force capturing. Do *not* commit code in this state, as this means +capturing will occur on all environments. + +Once enabled, this command will save a screenshot to `./screenshots/my-unique-string-12345678.jpg` +and the HTML markup to `./pages/my-unique-string-1234578.html`, where `12345678` +is the current timestamp. You may need to create those directories yourself and +give proper write permissions. + +Alternatively, you can use `casper.captureSelector('filename', 'div.some-class');` +to take a screenshot of a given selector. In this case, specify a full filename +to save the capture to, such as `screenshots/my-selector-12345678.jpg`. You can +find more examples about capturing [here](http://docs.casperjs.org/en/latest/modules/casper.html#capture). + +### Debugging + +If you would like to see a more verbose output of the test suites, you can run +the `drush casperjs` command with the `-d` flag: + +```bash +drush casperjs -d +``` + +This will execute CasperJS with `log-level` set to `debug`. In your scripts, you +can log to this debug mode by using `casper.log('my message', 'debug');`. + +In addition, other helpful debugging tools are [`casper.debugPage()`](http://casperjs.readthedocs.org/en/latest/modules/casper.html#debugpage) +and [`casper.debugHTML()`](http://casperjs.readthedocs.org/en/latest/modules/casper.html#debughtml). + +### Evaluating code + +The [casper.evaluate()](http://docs.casperjs.org/en/latest/modules/casper.html#evaluate) +method (and its alternatives such as `casper.evaluateOrDie()`, `casper.thenEvaluate()` or +`test.assertEvaluate()`) are highly powerful since they will run JavaScript code +on the page just as if you were debugging with the browser's JavaScript console. diff --git a/casperjs.drush.inc b/casperjs.drush.inc new file mode 100755 index 0000000..66d2f64 --- /dev/null +++ b/casperjs.drush.inc @@ -0,0 +1,104 @@ + 'Wrapper for running CasperJS tests. Accepts extra options for casperjs command.', + 'callback' => 'drush_casperjs', + 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_SITE, + 'arguments' => array( + 'tests' => 'A comma separated list of tests to run. If not provided, all tests at the tests directory will be run.', + ), + 'options' => array( + 'test-root' => 'Overrides the default location of tests.', + 'url' => 'The base URL to use for test requests. Defaults to the URL set by Drush or the current Drush alias.', + 'includes' => 'Comma-separated list of files to include before each test file execution.', + 'cookies-file' => 'Sets the file name to store the persistent cookies. If not provided a random file in the system temporary directory will be used.', + ), + 'examples' => array( + 'drush casperjs --url=http://boiler.local' => 'Runs all tests located at the tests directory against http://boiler.local', + 'drush casperjs --url=http://boiler.local homepage.js' => 'Runs homepage.js test against http://boiler.local.', + 'export BOILER_TEST_CAPTURE=true && drush -v casperjs --url=http://boiler.local --log-level=debug' => 'Runs all tests against http://boiler.local with extra verbose logging and taking screenshots on failed assertions.', + ), + 'allow-additional-options' => TRUE, + ); + + return $items; +} + +/** + * Implements drush_COMMANDFILE(). + */ +function drush_casperjs($tests = NULL) { + + $command = 'casperjs test --verbose'; + + if (!drush_get_option('cookies-file')) { + $cookie_file = drush_tempnam('casper-cookie-'); + $command .= ' --cookies-file=' . drush_escapeshellarg($cookie_file); + } + + if (!drush_get_option('url')) { + $uri = drush_get_context('DRUSH_SELECTED_URI'); + $command .= ' --url=' . drush_escapeshellarg($uri); + } + + // Add include files. + $command .= ' --includes=' . drush_escapeshellarg(dirname(__FILE__) . '/includes/common.js,' . dirname(__FILE__) . '/includes/session.js'); + $command .= ' --pre=' . drush_escapeshellarg(dirname(__FILE__) . '/includes/pre-test.js'); + $command .= ' --post=' . drush_escapeshellarg(dirname(__FILE__) . '/includes/post-test.js'); + if ($includes = drush_get_option('includes')) { + $command .= ',' . $includes; + } + + // Set the root where tests are. Defaults to the tests directory + if (!$root = drush_get_option('test-root')) { + $root = drush_get_context('DRUSH_DRUPAL_ROOT') . '/../tests'; + } + + // If a list of tests to run were passed, append them to the command. + // Otherwise, just set the root where tests are located so CasperJS + // will run all of them. + $tests_to_run = $root; + if ($tests) { + $tests_to_run = ''; + foreach (explode(',', $tests) as $test_file) { + $tests_to_run .= ' ' . drush_escapeshellarg($root . '/' . $test_file); + } + } + $command .= ' ' . $tests_to_run; + + // Append additional CasperJS options to the command. + $args = array(); + foreach (drush_get_original_cli_args_and_options() as $arg) { + // Don't pass some options through. + if (strpos($arg, '--test-root') !== FALSE + || strpos($arg, '--simulate') !== FALSE + || strpos($arg, '--includes') !== FALSE + || strpos($arg, '-') !== 0) { + continue; + } + $args[] = $arg; + } + if (!empty($args)) { + $command .= ' ' . implode(' ', $args); + } + + echo $command . "\n"; + $result = drush_shell_proc_open($command); + if ($result !== 0) { + return drush_set_error('CASPERJS_TEST_FAILED', dt('Tests failed.')); + } + else { + drush_log(dt('Tests succeeded.'), 'success'); + return TRUE; + } +} diff --git a/includes/common.js b/includes/common.js new file mode 100755 index 0000000..b516fc5 --- /dev/null +++ b/includes/common.js @@ -0,0 +1,180 @@ +/** + * Helper methods for navigating through a Drupal site. + * + * This file is included automatically by the casperjs Drush command. + */ +var utils = require('utils'); +var f = utils.format; + +// Set the default timeout to 2 minutes, since the backend experience can be +// quite slow. If you change this value in a test suite, please set it back. +casper.options.waitTimeout = 120000; + +/** + * Run a test suite, ending all sessions when done. + */ +casper.boilerRun = function(time) { + casper.run(function(self) { + casper.boilerEndSession(); + self.test.done(); + }, time); +}; + +/** + * Listen to the open.location event in order to prepend the hostname. + * + * This filter will automatically prepend the full hostname that you are running + * tests against to a given path. For example, if you run + * casper.thenOpen('node/1'), it will convert it to + * http://myhostname/node/1 + */ +casper.setFilter('open.location', function(location) { + if (utils.isUndefined(location)) { + location = ""; + } + var cleanPath = location.replace(/^\//, ''); + return casper.cli.get('url') + '/' + cleanPath; +}); + +/** + * Set the viewport to a different breakpoint. + * + * @param string $size + * A breakpoint name. One of mobile, tablet, tablet-landscape or desktop. + */ +casper.thenChangeViewport = function (size) { + this.then(function () { + if (size === 'mobile') { + this.viewport(320, 400); + } else if (size === 'tablet') { + this.viewport(768, 1024); + } else if (size === 'tablet-landscape') { + this.viewport(1020, 1020); + } else if (size === 'desktop') { + this.viewport(1280, 1280); + } else { + test.fail('Responsive Check Not Properly Defined') + } + }); +}; + +/** + * Save page markup to a file. Respect an existing savePageContent function, if + * casper.js core introduces one. + * + * @param String targetFile + * A target filename. + * @return Casper + */ +casper.savePageContent = casper.savePageContent || function(targetFile) { + var fs = require('fs'); + + // Get the absolute path. + targetFile = fs.absolute(targetFile); + // Let other code modify the path. + targetFile = this.filter('page.target_filename', targetFile) || targetFile; + this.log(f("Saving page html to %s", targetFile), "debug"); + // Try saving the file. + try { + fs.write(targetFile, this.getPageContent(), 'w'); + } catch(err) { + this.log(f("Failed to save page html to %s; please check permissions", targetFile), "error"); + this.log(err, "debug"); + return this; + } + + this.log(f("Page html saved to %s", targetFile), "info"); + // Trigger the page.saved event. + this.emit('page.saved', targetFile); + + return this; +}; + +/** + * Capture the markup and screenshot of the page. NOTE: Capturing will only + * occur in one of two ways: either you pass true as the second argument to this + * function (not recommended, except for testing purposes) or if you have set + * the BOILER_TEST_CAPTURE environment variable to true, like so: + * + * $ export BOILER_TEST_CAPTURE=true + * + * @param string filename + * The name of the file to save, without the extension. + * @param boolean force + * Force capturing of screenshots and markup. + */ +casper.boilerCapture = function(filename, force) { + // If we are not capturing, simply return. + if (!this.boilerVariableGet('BOILER_TEST_CAPTURE', false) && !force) { + return; + } + // If we didn't get a filename, use an empty string. + if (utils.isFalsy(filename)) { + filename = ''; + } + // Otherwise, add a dash delimiter to the end. + else { + filename += '-'; + } + // Make the filename unique with a timestamp. + filename += new Date().getTime(); + var screenshot = 'screenshots/' + filename + '.jpg', + markup = 'pages/' + filename + '.html', + prefix = '', + screenshot_url = screenshot, + markup_url = markup; + // If we have a Drupal files directory available, use it. + if (casper.boilerVariableGet('BOILER_FILES_DIRECTORY')) { + prefix = casper.boilerVariableGet('BOILER_FILES_DIRECTORY') + '/testing/'; + screenshot_url = casper.boilerVariableGet('BOILER_FILES_URL') + '/' + screenshot; + markup_url = casper.boilerVariableGet('BOILER_FILES_URL') + '/' + markup; + } + this.capture(prefix + screenshot); + this.test.comment(f('Saved screenshot to %s.', screenshot_url)); + this.savePageContent(prefix + markup); + this.test.comment(f('Saved markup to %s.', markup_url)); +}; + +/** + * Some event listeners to log errors to boilerCapture. + */ +casper.on('error', function() { + casper.boilerCapture('error'); +}); +casper.on('step.error', function() { + casper.boilerCapture('step-error'); +}); +casper.on('step.timeout', function() { + casper.boilerCapture('step-timeout'); +}); +casper.on('waitFor.timeout', function() { + casper.boilerCapture('wait-for-timeout'); +}); +casper.on('started', function() { + // If we have http authentication credentials, use them. + if (casper.boilerVariableGet('BOILER_TEST_HTTP_USERNAME') && casper.boilerVariableGet('BOILER_TEST_HTTP_PASSWORD')) { + this.log("Using HTTP Authentication."); + casper.setHttpAuth(casper.boilerVariableGet('BOILER_TEST_HTTP_USERNAME'), casper.boilerVariableGet('BOILER_TEST_HTTP_PASSWORD')); + } +}); + +/** + * Retrieves an environment variable. + * + * @param String key + * The name of the variable to retrieve. + * @param defaultValue + * The default value to return if no environment variable exists. + * @return + * The value from the environment variable, or the default if none was found, + * or undefined if neither are found. + */ +casper.boilerVariableGet = function(key, defaultValue) { + var variables = require('system').env; + return utils.isUndefined(variables[key]) ? defaultValue : variables[key]; +}; + +// Comment about the status of the BOILER_TEST_CAPTURE variable. +if (casper.boilerVariableGet('BOILER_TEST_CAPTURE')) { + casper.test.comment('Capturing of screenshots and markup is enabled.'); +} diff --git a/includes/post-test.js b/includes/post-test.js new file mode 100755 index 0000000..4710490 --- /dev/null +++ b/includes/post-test.js @@ -0,0 +1,20 @@ +/** + * Kills sessions for all users. + */ +casper.test.begin("Logging out sessions for all users.", function suite(test) { + casper.start(); + + casper.then(function() { + casper.each(casper.boilerUsers, function(self, user) { + casper.boilerBeginSession(user); + casper.thenOpen('user/logout', function() { + test.assertHttpStatus(200, user.label + ' has successfully logged out.'); + }); + }); + }); + + casper.run(function() { + test.done(); + }); + +}); diff --git a/includes/pre-test.js b/includes/pre-test.js new file mode 100755 index 0000000..55e2a19 --- /dev/null +++ b/includes/pre-test.js @@ -0,0 +1,20 @@ +/** + * Creates sessions for all users. + */ +casper.test.begin("Creating sessions for all users.", function suite(test) { + casper.start(); + + casper.then(function() { + casper.each(casper.boilerUsers, function(self, user) { + casper.boilerCreateSession(user); + }); + }); + + casper.thenOpen('user/logout', function() { + test.assertHttpStatus(403, 'No session is active.'); + }); + + casper.run(function () { + test.done(); + }); +}); diff --git a/includes/session.js b/includes/session.js new file mode 100755 index 0000000..f7b6c17 --- /dev/null +++ b/includes/session.js @@ -0,0 +1,120 @@ +/** + * Session management tools for CasperJS. + */ + +var utils = require('utils'); +var cookieName; + +/** + * An object of user session cookies, keyed by unique name, ie, editor, writer. + */ +casper.boilerSessions = {}; + +/** + * An array of user objects. + * + * Type here an array of all the sessions you would like + * CasperJS to open when running casper.boilerRun(). + */ +casper.boilerUsers = [ + { + "key": "admin", + "label": "Administrator", + "name" : "admin", + "pass" : "password" + } +]; + +/** + * Sign in a user using a set of credentials. + * + * @param Object user + * The user object to sign in as. + */ +casper.boilerSignIn = function(user) { + casper.thenOpen('user', function () { + this.fill('form#user-login', { + "name": user.name, + "pass": user.pass + }, true); + }); + + casper.waitForSelector('body.logged-in', function() { + this.log('Logged in as ' + user.label, 'info'); + }, function timeout() { + this.test.fail('Unable to log in as ' + user.label); + }); +}; + +/** + * Creates a session for a user. + * + * @param Object user + * The user object, with credentials. + */ +casper.boilerCreateSession = function(user) { + casper.boilerSignIn(user); + + // Store the cookie under a separate name, so we can do the rest of the + // tests as an anonymous user. + casper.then(function () { + casper.each(casper.page.cookies, function (self, cookie) { + if (cookie.name.match(/^SESS/)) { + // Store the cookie name so we can use it later. + cookieName = cookie.name; + // Store the cookie. + casper.boilerSessions[user.key] = cookie; + // Delete the cookie. + casper.boilerEndSession(); + } + }); + }); + + casper.thenOpen('user', function () { + var success = !this.exists('body.logged-in') && utils.isObject(casper.boilerSessions[user.key]); + this.test.assert(success, user.label + ' session cookie has been stored.'); + }); +}; + +/** + * Begin a session as a user specified by key. + * + * @param String key + * The key of the user, such as editor, writer, etc. + */ +casper.boilerBeginSession = function(key) { + casper.then(function() { + var label = key; + // If a full user object was passed, parse it for just the key. + if (!utils.isUndefined(key.key)) { + label = key.label; + key = key.key; + } + if (utils.isUndefined(casper.boilerSessions[key])) { + this.test.fail("Unable to find session for user " + key); + } + casper.page.addCookie(casper.boilerSessions[key]); + this.log('Began session as ' + label, 'info'); + }); +}; + +/** + * End a session, switching back to an anonymous user. + */ +casper.boilerEndSession = function() { + casper.then(function() { + casper.page.deleteCookie(cookieName); + this.log('Ended authenticated session.', 'info'); + }); +}; + +/** + * Start a test suite, signed in as a certain user. + * + * @param String key + * The key of the user, such as editor, writer, etc. + */ +casper.boilerStartAs = function(key) { + casper.start(); + casper.boilerBeginSession(key); +};