19 Test and Debug Oracle JET Apps

Test and debug Oracle JET web apps using your favorite testing and debugging tools for client-side JavaScript apps.

Test Oracle JET Apps

Use third-party tools such as QUnit or Selenium WebDriver to test your Oracle JET app.

Test Apps

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

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/v14.0.0/redwood/oj-redwood-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 apps 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.

Use BusyContext API in Automated Testing

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

The purpose of the BusyContext API is to accommodate sequential dependencies of asynchronous operations. Typically, you use 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 app 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 app having multiple Oracle JET components rather than a single component.

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

    Choose a busy context scoped for a DOM node when your app 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 app 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 = document.querySelector("#mycontext");
    var busyContext = Context.getContext(node).getBusyContext();    
    

Determining the Ready State

After obtaining a busy context, the next step is to inquire the busy state. 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 Context.getPageContext().getBusyContext().getBusyStates().join('\\n');";
    Object busyStatesLog = ((JavascriptExecutor)webDriver).executeScript(evalString);
    String retValue = "";
    if (busyStatesLog != null){
      retValue = busyStatesLog.toString();
      Assert.fail("waitForJetPageReady failed - !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 app opt'd in on the whenReady wait for bootstrap?
// 2) If the app 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'] && 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'] ? " +
"(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
  Context.setBusyContextDefaultTimeout(18000);

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

  var popup = document.getElementById("popup1");  
                                                                                  
  // busy context scoped for the popup
  var busyContext = 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.

    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 app flow depends on its completion.

  4. Resolve the busy state when the operation completes.

The app is responsible for releasing the busy state. The app 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.

Debug Oracle JET Applications

Since Oracle JET web apps are client-side HTML5 applications written in JavaScript, you can use your favorite browser's debugging facilities.

Debug Web Applications

Use your source code editor and browser's developer tools to debug your Oracle JET app.

Developer tools for widely used browsers like Chrome, Edge, and Firefox provide a range of features that assist you in inspecting and debugging your Oracle JET app as it runs in the browser. Read more about the usage of these developer tools in the documentation for your browser.

By default, the ojet build and ojet serve commands use debug versions of the Oracle JET libraries. If you build or serve your Oracle JET app in release mode (by appending the --release parameter to the ojet build or ojet serve command), your app uses minified versions of the Oracle JET libraries. If you choose to debug an Oracle JET app that you built in release mode, you can use the --optimize=none parameter to make the minified output more readable by preserving line breaks and white space:

ojet build --release --optimize=none
ojet serve --release --optimize=none

Note that browser developer tools offer the option to "pretty print" minified source files to make them more readable, if you choose not to use the --optimize=none parameter.

You may also be able to install browser extensions that further assist you in debugging your app.

Finally, if you use a source code editor, such as Visual Studio Code, familiarize yourself with the debugging tools that it provides to assist you as develop and debug your Oracle JET app.