Test context
Understand how the Desk test library simulates an interactive runtime environment for testing, including UI interaction and navigation.
Overview
The Desk framework test library is designed to run both unit and integration tests from the command line, including tests for UI interaction and navigation. The component that facilitates this is the test context, which simulates an interactive runtime environment.
The test context works entirely in-memory, without running an actual browser, native shell, or even a partial API such as the Document Object Model (DOM). Instead, it only implements the APIs that are part of the Desk framework itself, and then allows your code to ‘query’ and validate the resulting state (including UI).
The following example demonstrates how a test could activate a regular activity, interact with the simulated UI, and verify that the expected output is rendered.
describe("Example", (scope) => {
// set up a new test context before each test
let activity: CounterActivity;
scope.beforeEach(() => {
useTestContext();
activity = new CounterActivity();
app.addActivity(activity, true);
});
// test that the counter is 0 both in the activity and view
test("Counter starts at zero", async (t) => {
expect(activity.count).toBe(0);
await t.expectOutputAsync(100, { text: "Count: 0" });
});
// test that the Up button works
test("Counter goes up", async (t) => {
// find the button and click it
await t
.expectOutputAsync(100, { type: "button", text: "Up" })
.then((b) => b.getSingle().click());
// check that the counter is 1 both in the activity and view
expect(activity.count).toBe(1);
await t.expectOutputAsync(100, { text: "Count: 1" });
});
});
Initializing the test context
To start testing your application using the test context, you’ll need to register both the simulated renderer and navigation controller with the global application context. You can use the useTestContext() function from the test library package, which sets up the global context using the necessary classes.
Afterwards, the global context (i.e. app
) conforms to the TestContext type, which in turn refers to the TestRenderer and TestNavigationController classes.
To specify additional options for the test context, you can pass a configuration object or a callback that modifies the default test context options (an object of type TestContextOptions).
- function useTestContext(config?)Clears the current global app context and initializes a test context.
- type TestContextType definition for the global app context with test-specific render and activity contexts, set by the useTestContext function.
- class TestContextOptionsA class that contains options for the test context.
// use the test context with additional options:
let app = useTestContext((options) => {
options.captureLogs = true;
options.navigationPageId = "home";
});
app.renderer; // => instance of TestRenderer
Testing user navigation
After registering the test context, all navigation actions are handled by the TestNavigationController class. The test navigation controller behaves similarly to a ‘real’ navigation controller, i.e. it manages its own navigation stack and allows you to route between activities using e.g. app.navigate().
- class TestNavigationControllerA class that encapsulates the current navigation location, simulating browser-like behavior.
You can validate that your app has navigated to an expected path (page ID and detail), using the following method on the TestCase object. This method is asynchronous, and waits for a specified amount of time to check that the current page ID and detail match the expected values.
- expectNavAsync(timeout, pageId, detail?)Waits for the global navigation location to match the given page ID and detail.
// in a test case, navigate (async) and wait for it to complete:
app.navigate("home");
await expectNavAsync(100, "home");
// ... now, the current page is "home",
// or the test case would have failed after 100ms
To simulate user navigation instead (i.e. a user using the URL bar or clicking the back button in a browser), you’ll need to use the following methods of the test navigation controller directly. These methods change the current location synchronously, and allow the activity context to respond in turn (asynchronously).
- userBack()Removes the last path in navigation history immediately, simulating external ‘back’ navigation.
- userNavigation(pageId, detail?)Sets the provided location immediately, simulating external navigation.
Testing UI rendering
The test context simulates the rendering of UI components using the TestRenderer class. The renderer manages its own in-memory UI element tree (similar to the DOM — but with a minimal API), while no graphical rendering is actually performed.
- class TestRendererA class that represents an in-memory application render context.
The ‘rendered’ elements are instances of the TestOutputElement class. These elements can be queried using TestCase methods (see below), and a full JSON representation of the rendered tree can be obtained using the TestRenderer.getOutputDump() method.
- class TestOutputElementA class that represents a rendered output element.
- getOutputDump(…select)Returns an object representation of (selected) output.
Querying rendered elements
In order to test your application’s UI, you can query the rendered elements using the TestCase.expectOutputAsync() method.
This method waits for element(s) to be rendered (or fails after a specified timeout), and then returns an OutputAssertion object that allows you to validate the rendered output. The elements are matched using one or more objects with properties of the OutputSelectFilter type, each representing a filter — the first filter is applied, then the second one on all content of matching elements, and so on. This way, multiple elements may be found, which may not be part of the same container.
- expectOutputAsync(timeout, …select)Waits for output to be rendered (by the test renderer) that matches the provided filters.
- interface OutputSelectFilterAn object that provides filters to match a set of output elements, to be asserted using OutputAssertion.
- class OutputAssertionA class that provides assertion methods to be applied to a selection of rendered output elements.
// in a test case:
let out = await test.expectOutputAsync(100, {
type: "button",
text: "Click me",
});
out.elements; // => array of TestOutputElement objects
let buttonOut = out.getSingle(); // one button, or throws
buttonOut.text; // => "Click me"
Interacting with rendered elements
You can use TestOutputElement objects to simulate user interaction. Actions are typically limited to clicks and text input, but you can emit any DOM-like event on any element if needed. All events are emitted immediately, and are handled synchronously (e.g. invoking an activity method for a button click, before the .click()
method returns).
- click()Simulates a user click or tap event.
- setValue(text)Simulates text input on a text field element.
- sendPlatformEventHandles the specified (simulated) platform event.
// ... after finding a button element:
buttonOut.click();
// ... after finding a text field element:
textFieldOut.setValue("Hello, world!");
Note: As much as possible, mouse button, focus, and click event sequences are simulated by the test context as if the user had interacted with the UI using a mouse. The
.click()
and.setValue()
methods therefore emit several events in sequence.
Interacting with message dialogs
To make it easier to interact with message dialogs (i.e. dialogs rendered using app.showAlertDialogAsync() and app.showConfirmDialogAsync()), rather than querying for nested labels and buttons you can use the TestCase.expectMessageDialogAsync() method.
This method waits for a message dialog to be rendered (or fails after a specified timeout), and then allows you to validate the dialog’s content and interact with it using a RenderedTestMessageDialog object.
- expectMessageDialogAsync(timeout, …match)Waits for an alert or confirm dialog to be rendered (by the test renderer).
- class RenderedTestMessageDialogA class that represents a rendered message dialog (for testing).
The returned RenderedTestMessageDialog object encapsulates the rendered dialog, including its labels and buttons (as TestOutputElement objects). You can then simulate user interaction using its (asynchronous) methods, to click one of the buttons and wait for the dialog to disappear.
- confirmAsync()Clicks the first button of the dialog (confirm or dismiss button).
- cancelAsync()Clicks the last button of the dialog (cancel or dismiss button).
- clickAsync(button)Clicks the specified button (matched using the button label).
// in a test case:
let p = app.showConfirmDialog("Are you sure?");
let dialog = await expectMessageDialogAsync(100);
await dialog.cancelAsync();
// with the dialog closed, the promise resolves to false
let result = await p;
expect(result).toBe(false);