Hobbes.js

“ The best companion for your UI Testing adventure ”

Javascript based UI Testing framework for AEM related products.

Adding Tests

Steps:

  1. Register a "Test js file" Clientlib in the Framework.
  2. Create/Register Test Classes.
  3. Add Test Cases.
  4. Add Actions to a TestCase.

1. Register a "Test js file" Clientlib in the Framework

In CQ, create a new clienlib (cq:ClientLibraryFolder node), with specific properties:

  • categories : granite.testing.hobbes.tests
  • dependencies : granite.testing.hobbes.testrunner

AEM Hobbes.js testrunner (/libs/granite/testing/hobbes.html) now implements in basic filter system based on clientlib category. In order to filter test clientlibs to load, append additionnal categories to granite.testing.hobbes.tests. Example: granite.testing.hobbes.tests.myFeature

Then, use filter URL parameter in the testrunner (hobbes.html?filter=granite.testing.hobbes.tests.myFeature).

2. Create/Register Test Classes

hobs.TestSuite(name, options)
Parameter Description Default Example
name Name of the Testsuite (displayed in the test sidekick) - "My Test Suite"
options Object parameter null {path: "/etc/clientlibs/qe/hobbes-js-my-tests/MyTestSuite.js", register: false}

options accepts following properties:

Property Description Default Example
path Absolute path to the js file of the testsuite (Used for CRXDE lite navigation) - "/etc/clientlibs/qe/hobbes-js-my-tests/MyTestSuite.js"
register Controls registration of a TestSuite to the test sidekick. All registered TestSuites will be executed during on automated test run. Set this parameter to false to exclude a TestSuite from automated test run true true / false
delay Step delay applied to all test cases of the test suite (in ms.) - 2500
demoMode Controls "Demo Mode" activation. Applied to all test cases of the test suite false true / false
execBefore Registered test case executed before each test case of the test suite - beforeRegisteredTestCase
execAfter Registered test case executed after each test case of the test suite - afterRegisteredTestCase
execInNewWindow If true, testsuite testcases will run in a new window false true / false
winOptions together with execInNewWindow, this define the new window options width=1220, height=900, top=30, left=30 width=800, height=600, top=300, left=300
Example

Inside the clientlib, create MyTestSuite.js file and copy/paste the following code:

new hobs.TestSuite("MyTestSuite", {
    path: "<PATH_TO_YOUR_CLIENTLIB>/MyTestSuite.js",
    register: false,
    delay: 2500,
    execBefore: beforeMethod,
    execAfter: afterMethod
})

Save the file and reload CQ home page => You should see MyTestSuite in the test sidekick.

3. Add Test Cases

hobs.TestCase(name, options)
Parameter Description Default Example
name Name of the TestCase (displayed in the test sidekick) - "My Test Case"
options Object parameter null {delay: 2500, demoMode: true}

options accepts following properties:

Property Description Default Example
delay Additionnal delay between test steps, in ms. null (no delay) 2500 (2.5s delay)
execBefore Registered test case executed before the test case - beforeRegisteredTestCase
execAfter Registered test case executed after the test case - afterRegisteredTestCase
Example

In your test suite file:

new hobs.TestSuite("MyTestSuite", {path: "<PATH_TO_YOUR_CLIENTLIB>/MyTestSuite.js"})
    .addTestCase(new hobs.TestCase("myTestCase")
    )
);

Save the file and reload CQ home page => MyTestClass should now list myTestCase

TestSuite class implements chaining so to add multiple test cases:

new hobs.TestSuite("MyTestSuite", {path: "<PATH_TO_YOUR_CLIENTLIB>/MyTestSuite.js"})
    .addTestCase(new hobs.TestCase("myTestCase #1")
    )

    .addTestCase(new hobs.TestCase("myTestCase #2")
    )

    .addTestCase(new hobs.TestCase("myTestCase #3")
    )
);

Save the file and reload CQ home page => MyTestClass should now list myTestCase #1 myTestCase #2 myTestCase #3

4. Add Actions to a TestCase

TestCase class also implements chaining to ease writting process:

new hobs.TestSuite("MyTestSuite", {path: "<PATH_TO_YOUR_CLIENTLIB>/MyTestSuite.js"})
    .addTestCase(new hobs.TestCase("myTestCase #1")
        .execSyncFct(function() { hobs.utils.BrowserUtils.createCookie('wcmmode', 'preview', 1); })
        .navigateTo("/content/geometrixx-outdoors/en/men.html")
        .click('a[href="/content/geometrixx-outdoors/en/men/shirts/ashanti-nomad.html"]', {expectNav: true})
        .fillInput('[name="product-quantity"]', '5')
    )
);

Chaining gives us control over the test execution. All the test actions have been conceived in a synchronous way!

This way you don't have to add waiting times between test steps.

I.E. .click("jquery_selector)

the click function, before doing the click action on the element, will check its existence for 2.5s, every .25s!

  • As soon as element exists => Click is done => LOG STEP PASSED => GO TO NEXT STEP
  • If element does not exist after 2.5s timeout => NO Click action => LOG STEP FAILED => GO TO NEXT STEP

A Complete CQ Test


// TestCase: testBuyProduct
new hobs.TestCase("testBuyProduct")
    .execSyncFct(function() { hobs.utils.BrowserUtils.createCookie('wcmmode', 'preview', 1); })
    .navigateTo("/content/geometrixx-outdoors/en/men.html")
    .click('a[href="/content/geometrixx-outdoors/en/men/shirts/ashanti-nomad.html"]', {expectNav: true})
    .fillInput('[name="product-quantity"]', '5')
    .click('input[type="submit"][value="Add to Cart"]', {expectNav: true})
    .click('a[href="/content/geometrixx-outdoors/en/user/checkout.html"]', {expectNav: true})
    .fillInput('[name="billing.firstname"]', "TestUserFirstName")
    .fillInput('[name="billing.lastname"]', "TestUserLastName")
    .fillInput('[name="billing.street1"]', "TestStreet")
    .fillInput('[name="billing.city"]', "Bucharest")
    .fillInput('[name="billing.state"]', "Bucharest")
    .fillInput('[name="billing.zip"]', "032459")
    .fillInput('[name="billing.country"]', "3")
    .click('.form_button_submit.cq-checkout', {expectNav: true})
    .fillInput('[name="payment.primary-account-number"]', "0000000000000000")
    .fillInput('[name="payment.name-on-card"]', "Card owner")
    .fillInput('[name="payment.ccv"]', "666")
    .fillInput('[name="payment.expiration-date-month"]', "12")
    .fillInput('[name="payment.expiration-date-year"]', "20")
    .click('.form_button_submit.cq-checkout', {expectNav: true})
    .asserts.isTrue(function(){ return hobs.window.location.href.indexOf("/thank-you.html") > -1;})

Existing Test Actions

Test actions are mostly based on the same principle:

  1. Select a DOM element (Using jQuery Selectors).
  2. Check element's attributes OR Execute an action on it.

Default / Common Test Actions have been added to ease test writting process. jsDoc is generated under doc/. (Github pages version)

Actions File Description Example
hobs.actions.Core.js Core Actions navigateTo, click
hobs.actions.Assertions.js Assertions isTrue, exists, isVisible, isInViewport

To avoid conflict, a namespace system has been implemented:

Actions File Namespace Usage Example
hobs.actions.Core.js - .click( ... ) .navigateTo( ... ) ...
hobs.actions.Assertions.js asserts .asserts.isTrue( ... ) .asserts.exists( ... ) ...

Advanced Concepts

Test Execution Context

Hobbes.js loads test pages in an iFrame

A dedicated "test runner" page loads Hobbes.js framework + testrunner UI + tests but the tests are executed inside an iframe. Thus, direct references to any element of the test page (in the iframe) are not possible (ex. window, document, $).

Though, it is possible to access the page loaded in the test iframe.

Hobbes.js provides context aware versions of these objects:

  • hobs.context().window (ie. to get test run window location information)
  • hobs.context().document
  • hobs.find(selector) (ie. to select DOM elements in the test run window)
  • hobs.find(selector, context) (ie. to select DOM elements in custom context, like an iframe inside the test page)

Example on how to change test execution context in a test case:

var defaultContextEl = null;
var resetContextTC = TestCase('Reset Context')
        .execFct(function() {
            hobs.setContext(defaultContextEl);
        });

TestCase('Execute actions in a different context', {
    // Force context reset even if test fails
    execAfter: resetContextTC
})
    .click('here')
    .mouseover('there')

    // Change test context
    .execFct(function() {
        // Save current context
        defaultContextEl = hobs.context().loadEl;
        hobs.setContext(hobs.find('iframe').get(0));
    })

    // from now on, all selectors will be looked in newly set context, the 'iframe' inside the test window iframe
    .click('button')
    .mouseover('element')

TestSuites/TestCases organization

Example: you have the following suites for Feature-A

var FA_TS1 = TestSuite("Feature-A basic tests")
    .add(TestCase("test navigation")
        // Test steps ...
    )
    .add(TestCase("test basic actions")
        // Test steps ...
    )
    .add(TestCase("test other actions")
        // Test steps ...
    );

var FA_TS2 = TestSuite("Feature-A Create elements")
    .add(TestCase("test create element typeA")
        // Test steps ...
    )
    .add(TestCase("test create element typeB")
        // Test steps ...
    );

var FA_TS3 = TestSuite("Feature-A Delete elements")
    .add(TestCase("test delete element typeA")
        // Test steps ...
    )
    .add(TestCase("test delete element typeB")
        // Test steps ...
    );

This is fine at first sight though, in that case, you cannot run all the Feature-A test suites at once. You would have to run each suite separately:

hobs.runTest("Feature-A basic tests");
hobs.runTest("Feature-A Create elements");
hobs.runTest("Feature-A Delete elements");

// OR

FS_TS1.exec();
// wait for execution end
FS_TS2.exec();
// wait for execution end
FS_TS2.exec();
// wait for execution end

To solve this, you can actually a TestSuite inside another TestSuite: (based on elements defined above)

var FS_TS = TestSuite("Feature-A")
    .add(FA_TS1)
    .add(FA_TS2)
    .add(FA_TS3);

Then, to execute all Feature-A tests:

hobs.runTest("Feature-A");

// OR

FS_TS.exec();

This process is handled in the Testrunner UI though, to ensure that your TestSuite is not registered twice in the UI, you have to set register options parameter to false:

var FA_TS1 = TestSuite("Feature-A basic tests", null, {register: false})
    // [...]
var FA_TS2 = TestSuite("Feature-A Create elements", null, {register: false})
    // [...]
var FA_TS3 = TestSuite("Feature-A Delete elements", null, {register: false})
    // [...]

// then
var FS_TS = TestSuite("Feature-A")
    .add(FA_TS1)
    .add(FA_TS2)
    .add(FA_TS3);

This will create the following structure in the Testrunner UI:

> Feature-A

    > Feature-A basic tests

        - test navigation
        - test basic actions
        - test other actions

    > Feature-A Create elements

        - test create element typeA
        - test create element typeB

    > Feature-A Delete elements

        - test delete element typeA
        - test delete element typeB

Use TestCase Inside TestCases

Some part of test cases can be repetitive:

  • Preparing the environment (i.e. creating a folder, navigating to a specific location, setting properties, ...)
  • Cleaning the environment (i.e. deleting a folder, deleting resources, ...)
  • ...

Hobbes.js implements a sub chaining process which allows you to register a specific TestCase (chain of actions), outside of a TestSuite, that you can then execute in any TestCase.

Register a TestCase as a subchain (outside of a TestSuite)

var createAssetFolderSubChain = new hobs.TestCase("createAssetFolderSubChain")
    .navigateTo("/assets.html")
    .click("a.cq-damadmin-admin-actions-createfolder-activator")
    .typeInput("input#foldertitle", hobs.testData.folderTitle)
    .click("button#createfolder-submit")
    .asserts.exists("article[data-type='directory'][data-path='/content/dam/" + hobs.testData.folderId + "']", true, {timeout: 10000})

Then,

Execute registered TestCase inside another TestCase

.addTestCase(new hobs.TestCase("Check newly created asset folder is empty")
    .execTestCase(createAssetFolderSubChain)
    .click("article[data-type='directory'][data-path='/content/dam/" + hobs.testData.folderId + "'] a[data-foundation-content-history-title='" + hobs.testData.folderTitle + "']")
    .asserts.exists("div.no-children-banner.center")
    .asserts.exists("nav.toolbar nav.pulldown a:contains('" + hobs.testData.folderTitle + "')")
    .execTestCase(deleteAssetFolderChain)
)

.execTestCase(registeredTestCase)

Before / After Chains

Extending sub chain concept, we did a first implementation of jUnit Before/After concept in Hobbes.js.

At TestSuite and TestCase level, you can define in the options object parameter a before and/or an after sub chain to execute:

Type Level Behaviour
execBefore TestCase Executed before the TestCase
execBefore TestSuite Executed before each TestCases
execAfter TestCase Executed after the TestCase
execAfter TestSuite Executed after each TestCases
Example
var locationSetupBefore = new hobs.TestCase("locationSetupBefore")
    .navigateTo("/content/qe/hobbes-js-test-pages/index.html")

new hobs.TestSuite("TestSuite-with-BeforeMethod-Tests", {
    execBefore: locationSetupBefore
})

.addTestCase(new hobs.TestCase("Before method at TestSuite level - .navigateTo Action - Main Page")
    .asserts.location("/content/qe/hobbes-js-test-pages/index.html")
)

Dynamic Parameters

Best practice in general is to avoid hardcoded values. Hobbes.js provides a way to register variable in the framework that you can then use in TestCases:

Set parameters

hobs.param("navUrl", "/home/index.html");

Usage in Test elements

new hobs.TestCase("navigateChain")
    .navigateTo("%navUrl%")

At test execution, "%navUrl%" will be replaced with /home/index.html

Scopes of Dynamic Parameters

  • Global/Default scope
hobs.param("navUrl", "value");

Now, any "%navUrl%" references in Test elements will be replaced by value during test execution

  • Test Elements scope

In some Test elements you want to use a different URL value than the default one:

// Set default value for navUrl parameter
hobs.param("navUrl", "/projects.html/");

new hobs.TestSuite("Navigate using Dynamic Parameters")

    .addTestCase(new hobs.TestCase("Test Default Parameter")
        .navigateTo("%navUrl%")
        .asserts.location("/projects.html/")
        // => At test execution, `"%navUrl%"` will be replaced with default value /home/index.html
    )

    .addTestCase(new hobs.TestCase("Test Parameter Set at TestCase Level",
        // Override navUrl value for this TestCase
        { params: { navUrl: "/assets.html/content/dam" } }
    )
        .navigateTo("%navUrl%")
        .asserts.location("/assets.html/content/dam")
        // => At test execution, `"%navUrl%"` will be replaced with /assets.html/content/dam
    )

    .addTestCase(new hobs.TestCase("Test Parameter Set at Test action Level")
        // Override navUrl value for this TestAction only!
        .navigateTo("%navUrl%", { params: { navUrl: "/screens.html/content/screens" } })
        .asserts.location("/screens.html/content/screens")
        // => At test execution, `"%navUrl%"` will be replaced with "/screens.html/content/screens"

        .navigateTo("%navUrl%")
        .asserts.location("/projects.html/")
        // => At test execution, `"%navUrl%"` will be replaced with default parameter value "/projects.html/"
    );

Also, using in string annotation, you can build more complex parameters:

// BTW, no need to set default value for parameters...
new hobs.TestSuite("Test Dynamic Parameters", {
        params: {
            "TestSuiteUrlSuffix": "/content/screens/geometrixx/channels"
        }
    })

    .addTestCase(
        new hobs.TestCase("in-string dyn. parameter", {
            params: {
                "urlSuffix": "/content/screens"
            }
        })
        .navigateTo("/screens.html%TestSuiteUrlSuffix%")
        .asserts.location("/screens.html/content/screens/geometrixx/channels")
        // => At test execution, `"%TestSuiteUrlSuffix%"` will be replaced with value set in TestSuite "TestSuiteUrlSuffix"
        // So navigation will be done to URL "/screens.html/content/screens/geometrixx/channels"

        .wait(500)
        .navigateTo("/screens.html%urlSuffix%")
        .asserts.location("/screens.html/content/screens")
        // => At test execution, `"%urlSuffix%"` will be replaced with value set in TestCase
        // So navigation will be done to URL "/screens.html/content/screens"

        .wait(500)
        .navigateTo("/screens.html%urlSuffix%", {params: {"urlSuffix": "/content/screens/geometrixx/locations/demo/flagship"}})
        .asserts.location("/screens.html/content/screens/geometrixx/locations/demo/flagship")
        // => At test execution, `"%urlSuffix%"` will be replaced with value set in the test action itself
        // So navigation will be done to URL "/screens.html/content/screens/geometrixx/locations/demo/flagship"
    );
Examples
/**
 * First of all! Register the parameters you expect to use!
**/
hobs.param("navUrl", "/communities.html");

/**
 * Define subChain that uses "navUrl" as parameter
**/
var navigateChain = new hobs.TestCase("navigateChain")
    .navigateTo(hobs.param("navUrl"))


/**
 * Create a TestSuite
 * + Setting "navUrl" parameter at TestSuite level
**/
new hobs.TestSuite("DynamicParameters-TestSuite-With-Param",
    {
        path: "/etc/clientlibs/qe/hobbes-js-sample-tests/generic/DynamicParametersTests/DynamicParametersTests.js",
        params: {
            navUrl: "/sites.html/content"
        }
    }
)

/**
 * Create a TestCase
 * + Setting "navUrl" parameter at TestCase level (will overwrite TestSuite parameter value)
**/

.addTestCase(new hobs.TestCase("Test Parameter Set at TestCase Level",
        {
            params: {
                navUrl: "/assets.html/content/dam"
            }
        }
    )
    .navigateTo(hobs.param("navUrl"))
    .asserts.location("/assets.html/content/dam")
)


/**
 * Create a TestCase
 * No parameter set, "navUrl" will get value From TestSuite
**/
.addTestCase(new hobs.TestCase("Test Parameter Set at TestSuite Level")
    .navigateTo(hobs.param("navUrl"))
    .asserts.location("/sites.html/content")
)