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.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:
- 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().document
hobs.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 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")
)