Hobbes.js
“ The best companion for your UI Testing adventure ”
Javascript based UI Testing framework for AEM related products.
Adding Tests
Steps:
- Register a "Test js file" Clientlib in the Framework.
- Create/Register Test Classes.
- Add Test Cases.
- 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.testsdependencies: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:
- Select a DOM element (Using jQuery Selectors).
- 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().documenthobs.find(selector)(ie. to select DOM elements in the test run window)hobs.find(selector, context)(ie. to select DOM elements in customcontext, 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 endTo 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 typeBUse 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")
)