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.
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")
)