Testing Oracle JET Applications

Use third-party tools such as QUnit or Selenium WebDriver to test your web application. Google, Apple, and Microsoft also provide instructions for testing Andoid, iOS, and Windows applications. To test Oracle JET components, the framework provides the getNodeBySubId() and getSubIdByNode() methods to test Oracle JET components.

Testing Applications

You can use virtually any testing tool that tests client-side HTML applications written in JavaScript for testing Oracle JET applications.

For internal development, Oracle JET uses the following tools for testing Oracle JET components and framework features:

  • QUnit: JavaScript unit testing framework capable of testing any generic JavaScript project and used by the jQuery, jQuery UI, and jQuery Mobile projects.

    QUnit requires configuration on your test page to include library and CSS references. You must also add the HTML div element to your page. In the example below, the highlighted code shows additions required by QUnit. The actual paths will vary, depending upon where you install QUnit.

    <!doctype html>
    <html lang="en">
      <head>
        <link rel="stylesheet" href="../../../../code/css/libs/oj/vx.x.x/alta/oj-alta-min.css"></link>
        <link rel="stylesheet" href="../../../css/qunit.css">
        <script type="text/javascript" src="../../../js/qunit.js"></script>
        <script>
          QUnit.config.autoload = false;
          QUnit.config.autostart = false;
        </script>        
        <script data-main="js/main" src="../../../../code/js/libs/require/require.js"></script>
        </head>
      <body>
        <div id="qunit"></div>
        <div id="qunit-fixture">
          <input id="slider1"></input>
        </div> 
      </body>
    </html>
    

    This example also defines an HTML input element in the body element. The QUnit test script creates the slider and defines a hands-off test that simulates a drag event using the jquery-simulate jQuery plugin. The code uses the getNodeBySubId() method to access the slider’s thumb.

    (function($) {
        module( "slider: events" );
    
        test( "drag test offsets ", function() {
            expect(10);
            var element = $( "#slider1" )
                .ojSlider({
                    optionChange: function(event, ui) {
                        if (ui.option == "value")
                            ok( true, "value change triggered by dragstop " + ui.value);
                        if (ui.option == "rawValue")
                            ok( true, "rawValue change triggered by dragstop " + ui.value);
                        var thumb =  $('#slider1').ojSlider("getNodeBySubId", {subId:"oj-slider-thumb-0"});
    
                        // Make sure that the thumb top is set to "".
                        equal($(thumb)[0].style.top, "", " thumb top correctly set to empty");
                    }
                });
            var thumb = element.ojSlider("getNodeBySubId", {subId:"oj-slider-thumb-0"});
            $(thumb).simulate( "drag", { dx: 10, dy: 0 } );
        });
    })( jQuery );
    

    For more information about working with getNodeBySubId() to test Oracle JET components, see Using getNodeBySubId() in Testing.

    For more information about QUnit, see http://qunitjs.com.

  • Selenium WebDriver: Alternative method of testing applications that you do not want to modify for QUnit or that contain multiple HTML pages.

    For additional information, see http://docs.seleniumhq.org/projects/webdriver.

Testing Hybrid Mobile Applications

You can test your hybrid mobile application in a local browser, using an emulator or simulator, and on an attached device.

  • Testing in a local browser

    When you use grunt with the –-browser option to serve your hybrid mobile application in a local browser, you can use the same method for testing Android, iOS, and Windows applications that you would use for testing any web application. However, this method can only approximate the end user experience and is most useful early in the development process.

    Depending upon your use case, you may need to add Cordova plugins to add functionality to your mobile hybrid app. Many Cordova plugins provide mock data when deploying to the browser platform. If, however, you add a Cordova plugin that doesn’t have browser platform support, you can add objects that represent mock data to cordovaMock.js in src/js.

  • Testing in an emulator or simulator

    You can use grunt serve to test the functionality of your application in the iOS Simulator, Windows emulator, or Android Virtual Devices (AVDs) using the Android emulator. These methods can approximate the look and feel of an actual device, but you won’t be able to test performance or responsiveness to touch reliably. For additional information, see

  • Testing on attached physical devices

    You can use grunt serve with the destination=device option to test the functionality on attached physical devices. This provides the most reliable form of testing, but you may not have access to all the devices that your users might use to run your application.

    However, if you want to send your application to an iOS device, you must take additional steps as described in Packaging and Publishing Hybrid Mobile Applications. Typically, you can develop and test your iOS application in the Simulator or web browser until you’re ready to build and test your release candidate.

For additional information about testing hybrid mobile applications on specific platforms, see the following resources:

Testing Oracle JET Components

To assist you with testing Oracle JET components in your own application, Oracle JET provides the getNodeBySubId() and getSubIdByNode() methods which provide safe access to an Oracle JET component’s internal parts for testing.

  • getNodeBySubId(locator): Returns the component DOM node indicated by the locator parameter.

  • getSubIdByNode(node): Returns the subId string for the given child DOM node.

    Method can be used by test authors to tie into test script generators that must retrieve a recordable ID from a live DOM component while recording a test. This method is supported by only a subset of Oracle JET components, and you should check the Oracle JET component's API to verify support.

Important:

Use these methods to access an Oracle JET component’s internal DOM nodes instead of other commonly-used testing techniques such as XPath. Oracle JET makes no guarantee that the internal DOM structure of a component won’t change.

Using getNodeBySubId() in Testing

The getNodeBySubId() method provides safe access to an Oracle JET component's internal parts to check values and manipulate components. Most Oracle JET components will consist of one or more internal parts, called nodes. For example, the Oracle JET ojSlider component has one or two thumb nodes that you can access with the getNodeBySubId() method.

To use the getNodeBySubId() method in testing:

  1. When you create your component, assign it an id selector in the HTML markup.

    The code below shows the example markup for the HTMLinput element shown in Testing Applications.

    <div id="qunit"></div>
    <div id="qunit-fixture">
        <input id="slider1"></input>
    </div> 
    

    Tip:

    This example uses QUnit for testing, but you can use any testing tool. Just be sure to assign an id selector to the element or component you are testing.

  2. Check the component's API documentation for the list of nodes that you can access with the getNodeBySubId() method.

    For example, if you want to find the node for the single thumb or the lower-valued thumb in an Oracle JET oSlider component, check the ojSlider API documentation for getNodeBySubId() and the list of supported Sub-IDs which represent the nodes. The documentation indicates that to access the thumb, use oj-slider-thumb-0.

  3. Add the code to access the thumb using getNodeBySubId() to your test script.

    For example, the following code accesses the slider’s thumb in the ojSlider example.

    var thumb = $('#slider1').ojSlider("getNodeBySubId", {subId:"oj-slider-thumb-0"});
    

Using oj.BusyContext API in Automated Testing

Use oj.BusyContext to wait for a component or other condition to complete some action before interacting with it.

The purpose of the oj.BusyContext API is to accommodate sequential dependencies of asynchronous operations. Typically, you use oj.BusyContext in test automation when you want to wait for an animation to complete, a JET page to load, or a data fetch to complete.

Wait Scenarios

The Busy Context API will block until all the busy states resolve or a timeout period lapses. There are four primary wait scenarios:

  • Components that implement animation effects

  • Components that fetch data from a REST endpoint

  • Pages that load bootstrap files, such as the Oracle JET libraries loaded with RequireJS

  • Customer-defined scenarios that are not limited to the Oracle JET framework, such as blocking conditions associated with application domain logic

Determining the Busy Context’s Scope

The first step for waiting on a busy context is to determine the wait condition. You can scope the granularity of a busy context for the entirety of the page or limit the scope to a specific DOM element. Busy contexts have hierarchical dependencies mirroring the document's DOM structure with the root being the page context. Depending on your particular scenario, target one of the following busy context scopes:

  • Scoped for the page

    Choose the page busy context to represent the page as a whole. Automation developers commonly need to wait until the page is fully loaded before starting automation. Also, automation developers are usually interested in testing the functionality of an application having multiple Oracle JET components rather than a single component.

    var busyContext = oj.Context.getPageContext().getBusyContext();
    
  • Scoped for the nearest DOM element

    Choose a busy context scoped for a DOM node when your application must wait until a specific component’s operation completes. For example, you may want to wait until an ojPopup completes an open or close animation before initiating the next task in the application flow. Use the data-oj-context marker attribute to define a busy context for a DOM subtree.

    <div id="mycontext" data-oj-context>
      ...
      <!-- JET content -->
      ...
    </div>
    
    var node = $("#mycontext");
    var busyContext = oj.Context.getContext(node[0]).getBusyContext();    
    

Determining the Ready State

After obtaining a busy context, the next step is to inquire the busy state. oj.BusyContext has two operations for inquiring the ready state: isReady() and whenReady(). The isReady() method immediately returns the state of the busy context. The whenReady() method returns a Promise that resolves when the busy states resolve or a timeout period lapses.

The following example shows how you can use isReady() with WebDriver.

public static void waitForJetPageReady(WebDriver webDriver, long timeoutInMillis)
{
  try
  {
    final WebDriverWait wait = new WebDriverWait(webDriver, timeoutInMillis / _THOUSAND_MILLIS);
    // Eat any WebDriverException
    // "ExpectedConditions.jsReturnsValue" will continue to be called if it doesn't return a value.
    // /ExpectedConditions.java#L1519
    wait.ignoring(WebDriverException.class);
    wait.until(ExpectedConditions.jsReturnsValue(_PAGE_WHEN_READY_SCRIPT));
  }

  catch (TimeoutException toe)
  {
    String evalString = "return oj.Context.getPageContext().getBusyContext().getBusyStates().join('\\n');";
    Object busyStatesLog = ((JavascriptExecutor)webDriver).executeScript(evalString);
    String retValue = "";
    if busyStatesLog != null)
      retValue = busyStatesLog.toString();
      Assert.fail("waitForJetPageReady failed - !oj.Context.getPageContext().getBusyContext().isReady() - busyStates: " +
        retValue);  }
}
 
// The assumption with the page when ready script is that it will continue to execute until a value is returned or
// reached the timeout period.
//
// There are three areas of concern:
// 1) Has the application opt'd in on the whenReady wait for bootstrap?
// 2) If the application has opt'd in on the jet whenReady strategy for bootstrap "('oj_whenReady' in window)",
// wait until jet core is loaded and have a ready state.
// 3) If not opt-ing in on jet whenReady bootstrap, make the is ready check if jet core has loaded. If jet core is
// not loaded, we assume it is not a jet page.

// Check to determine if the page is participating in the jet whenReady bootstrap wait period.

static private final String _BOOKSTRAP_WHEN_READY_EXP = "(('oj_whenReady' in window) && window['oj_whenReady'])";
 
// Assumption is we must wait until jet core is loaded and the busy state is ready.
static private final String _WHEN_READY_WITH_BOOTSTRAP_EXP =
"(window['oj'] && window['oj']['Context'] && oj.Context.getPageContext().getBusyContext().isReady() ?" +
" 'ready' : '')";

// Assumption is the jet libraries have already been loaded. If they have not, it's not a Jet page.
// Return jet missing in action "JetMIA" if jet core is not loaded.
static private final String _WHEN_READY_NO_BOOTSTRAP_EXP =
"(window['oj'] && window['oj']['Context'] ? " +
"(oj.Context.getPageContext().getBusyContext().isReady() ? 'ready' : '') : 'JetMAI')";

// Complete when ready script
static private final String _PAGE_WHEN_READY_SCRIPT =
"return (" + _BOOKSTRAP_WHEN_READY_EXP + " ? " + _WHEN_READY_WITH_BOOTSTRAP_EXP + " : " +
_WHEN_READY_NO_BOOTSTRAP_EXP + ");";

The following example shows how you can use whenReady() with QUnit.

// Utility function for creating a promise error handler
function getExceptionHandler(assert, done, busyContext)
{
  return function (reason)
    {
      if (reason && reason['busyStates'])
      {
        // whenReady timeout
        assert.ok(false, reason.toString());
      }
      else
      {
        // Unhandled JS Exception
        var msg = reason ? reason.toString() : "Unknown Reason";
        if (busyContext)
          msg += "\n" + busyContext;
        assert.ok(false, msg);
      }

      // invoke done callback
      if (done)
        done();
    };
};

QUnit.test("popup open", function (assert)

{
  // default whenReady timeout used when argument is not provided
  oj.BusyContext.setDefaultTimeout(18000);

  var done = assert.async();
  assert.expect(1);

  var popup = $("#popup1");  
  popup.ojPopup();
  
  // busy context scoped for the popup
  var busyContext = oj.Context.getContext(popup[0]).getBusyContext();
  var errorHandler = getExceptionHandler(assert, done, busyContext);

  popup.ojPopup();
  popup.ojPopup("open", "#showPopup1");

  busyContext.whenReady().then(function ()
  {
    assert.ok(popup.ojPopup("isOpen"), "popup is open");
    popup.ojPopup("close");
    busyContext.whenReady().then(function ()
    {
      popup.ojPopup("destroy");
      done();
    }).catch(errorHandler);
  }).catch(errorHandler);
});

Creating Wait Conditions

Jet components utilize the busy context to communicate blocking operations. You can add busy states to any scope of the busy context to block operations such as asynchronous data fetch.

The following high level steps describe how to add a busy context:

  1. Create a Scoped Busy Context.

  2. Add a busy state to the busy context. You must add a description that describes the purpose of the busy state. The busy state returns a resolve function which is called when it’s time to remove the busy state.

    Note:

    Busy Context dependency relationships are determined at the point the first busy state is added. If the DOM node is re-parented after a busy context was added, the context will maintain dependencies with any parent DOM contexts.
  3. Perform the operation that needs to be guarded with a busy state. These are usually asynchronous operations that some other application flow depends on its completion.

  4. Resolve the busy state when the operation completes.

Important:

The application is responsible for releasing the busy state. The application must manage a reference to the resolve function associated with a busy state, and it must be called to release the busy state. If the DOM node that the busy context is applied to is removed in the document before the busy state is resolved, the busy state will be orphaned and will never resolve.