Javascript, Protractor, Tests, WebDriver

Using angular.element with WebDriver’s executeScript to perform actions which aren’t accessible from UI


A while ago i had the following problem:
I needed to write UI test of a canvas based component, which received array of objects, for the sake of the post let’s assume those were cars objects, and draw DOM elements on the canvas to represent those cars. It also had functionality to display car information in a popup when a user clicks on a car or a model in scope is updated (let’s call it vm.selectedCar).

I needed to test the information popup functionality.
The problem was that i didn’t have anything to work with, because:

  1. I didn’t have any element to select, because there were no DOM elements on the canvas.
  2. I couldn’t rely on mouse actions because elements position on the canvas was inconsistent by design.

So basically i couldn’t do anything on a UI level to trigger the appereance of the information popup.

executeScript

As you may know, you can execute javascript by using WebDriver’s executeScript, so if we combine it with angular.element ability to retrieve the scope of the currently focused element, we can basically perform actions on the scope that we can’t do by interacting with the UI. Let’s see some code:

var openCarInfoTroughConsole = function(args) {
        var carsCanvasScope = angular.element($("#"+args.canvasId)).scope();
        carsCanvasScope.vm.selectedCar = args.carId;
        carsCanvasScope.$apply();
    };

var mockCar = {
        canvasId: 'canvasElmId',
        selectedCar: 'carId33'
};

browser
    .executeScript(openCarInfoTroughConsole, mockCar)
    .then(performTests, handleError);

As you can see here, the first argument passed to executeScript, is a function that will be executed in the browser console scope (you can also pass a string).
The second param is arguments that we can pass from the test scope to the function scope when it’ll be executed, in this case we use those arguments to pass our car mock Id.

The openCarInfoTroughConsole itself using the angular.element($0).scope() / isolateScope() trick to get the scope of the selected element. But in this case, instead of using $0 we locate our element by id.

Important to note – depending on your angular version, debugInfoEnabled may be set to false. The app must run with debug info enabled, otherwise scope() or isolateScope() will be undefined. Since in most cases you’ll run tests in development enviroment, it shouldn’t to be a problem.

After we got the scope of our component, we are updating selectedCar model with the id of our mock car and then triggering $apply to notify angular that we changed the model. After this, the information popup will be displayed and you’ll be able to run tests on it. That’s it.

Continous Integration, Grunt, Javascript, Tests, Xvfb

Running UI tests on real browsers in continuous integration using X virtual framebuffer (trough a task-runner)


Many times when projects run UI tests as part of CI (continuous integration) they rely on PhantomJS, a great headless browser. Unfortunately, using PhantomJS has some drawbacks:

  1. You really don’t cover quirks happening from browser to browser (working on chrome, not on FF, working on FF, not working on IE), so you might actually ship a buggy screen to production
  2. Debugging is hard. Although PhantomJS runs on top of WebKit, it has it own quirks, so you might get failing tests while all of your browsers show that everything’s OK. Debugging something you can’t see is troublesome.

UI tests with real browsers

So you’re not completely satisfied with PhantomJS and you want to run your UI tests on real browsers, which is a good idea that have a little difficulty (or not): you want your UI tests to run as part of CI, but most of CI servers don’t have displays.

Enter Xvfb (X virtual framebuffer). From Wikipedia: Xvfb is a display server implementing the X11 display server protocol. In contrast to other display servers, Xvfb performs all graphical operations in memory without showing any screen output. From the point of view of the client, it acts exactly like any other X display server, serving requests and sending events and errors as appropriate. However, no output is shown. This virtual server does not require the computer it is running on to have a screen or any input device. Only a network layer is necessary.

So basically what you’ll need to do is:

  1. Start Xvfb
  2. Run your tests
  3. Kill Xvfb

Running Xvfb trough a task runner

In this example we’ll use Grunt to run Xvfb as one of the tasks, but this is also possible with Gulp (I’m not sure about the others).
First, we’ll install the required packages, make sure you’re not forgetting to –save-dev since we want to update our package.json with the new dependency for development.

npm install --save-dev grunt-env
npm install --save-dev grunt-shell-spawn

Next we’ll setup grunt tasks for Xvfb:

grunt.initConfig({
        shell: {
            xvfb: {
                command: 'Xvfb :99 -ac -screen 0 1600x1200x24',
                options: {
                    async: true
                }
            }
        },
        env: {
            xvfb: {
                DISPLAY: ':99'
            }
        }
});

Now we’ll setup the UI test task, for the example we’ll use protractor but it can be any other library.

protractor: {
            options: {
                keepAlive: false,
                configFile: "protractor.conf.js"
            },
            run: {}
        },

In protractor.conf.js we should create configuration to run UI tests on the required browsers (I assume you have already the proper setup for this, since it’s not in the scope of this post).

Now we got:

  1. Grunt test task configuration
  2. Protractor configuration which will run the tests on all the required browsers
  3. Grunt Xvfb tasks configuration

Let’s create a task to combine everything together:

grunt.registerTask('CI-E2E', ['shell:xvfb', 'env:xvfb', 'protractor:run', 'shell:xvfb:kill']);

After this, a can simply run

CI-E2E

and all your UI tests will run on top of Xvfb. That’s it.