Testing Oracle JET Applications

Use third-party tools such as QUnit or Selenium WebDriver to test your Oracle JET application. Google, Apple, and Microsoft also provide instructions for testing Andoid, iOS, and Windows applications.

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 toolkit 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/v5.2.0/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">
          <oj-slider id="slider1"></oj-slider>
        </div> 
      </body>
    </html>
    

    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 invoke ojet serve 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 invoke ojet serve with the --emulator option 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 invoke ojet 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.

    If you want to serve your application to an iOS device, you must take additional steps as described in Packaging and Publishing Hybrid Mobile Applications.

  • Working around cross-origin resource sharing (CORS) issues

    Hybrid mobile applications that communicate with remote services may encounter issues if they request resources that originate in difference domains. An example includes a response such as the following:

    No 'Access-Control-Allow-Origin' header is present on the requested resource.

    To work around these types of issue, your server-side administrator may need to modify the policy used by the remote server to allow cross-site requests. For example, to access a remote service managed by Oracle’s Mobile Cloud Service (MCS), an MCS administrator configures MCS’s Security_AllowOrigin environment policy with a comma separated list of URL patterns that identify the remote services that serve a resource from different domains.

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

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 Oracle JET, 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 _BOOTSTRAP_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' : '') : 'JetMIA')";

// Complete when ready script
static private final String _PAGE_WHEN_READY_SCRIPT =
"return (" + _BOOTSTRAP_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 = document.getElementById("popup1");  
                                                                                  
  // busy context scoped for the popup
  var busyContext = oj.Context.getContext(popup).getBusyContext();
  var errorHandler = getExceptionHandler(assert, done, busyContext);

  popup.open("#showPopup1");

  busyContext.whenReady().then(function ()
  {
    assert.ok(popup.isOpen(), "popup is open");
    popup.close();
    busyContext.whenReady().then(function ()
    {
      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.