Testing
Testing rvx based applications is usually very simple because all of it's signal based rendering is synchronous. E.g. when updating a signal, all resulting changes are reflected in the DOM immediately:
import { $ } from "rvx";
const count = $(7);
const element = <div>Current count: {count}</div> as HTMLDivElement;
assert(element.innerText === "Current count: 7");
count.value = 42;
assert(element.innerText === "Current count: 42");
import { $, e } from "./rvx.js";
const count = $(7);
const element = e("div").append("Current count: ", count).elem;
assert(element.innerText === "Current count: 7");
count.value = 42;
assert(element.innerText === "Current count: 42");
Note, that the assert function used on this page is not included in rvx.
Synchronous Tests
Rvx provides a lightweight wrapper for running small synchronous tests that takes care of calling teardown hooks after the test.
import { $ } from "rvx";
import { runTest, querySelector } from "rvx/test";
runTest(() => {
const count = $(0);
const view = mount(
document.body,
<button on:click={() => { count.value++; }}>Click me!</button>,
);
querySelector(view, "button")?.click();
assert(count.value === 1);
});
import { $ } from "./rvx.js";
import { runTest, querySelector } from "./rvx.test.js";
runTest(() => {
const count = $(0);
const view = mount(
document.body,
e("button").on("click", () => { count.value++; }).append("Click me!"),
);
querySelector(view, "button")?.click();
assert(count.value === 1);
});
Asynchronous Tests
There is a wrapper for async tests that allows you to run small synchronous parts of your test with a shared async context. After the test, this will run teardown hooks registered during any "use(..)" calls and wait for any pending tasks tracked in the async context.
The example below shows a test that asserts that asynchronously loaded content is displayed correctly:
import { mount } from "rvx";
import { Async } from "rvx/async";
import { runAsyncTest, querySelector } from "rvx/test";
await runAsyncTest(async ({ asyncCtx, use }) => {
const view = use(() => {
return mount(
document.body,
<Async source={async () => {
await something();
return "Hello World!";
}}>
{content => <div class="page">
{content}
</div>}
</Async>
);
});
// Wait for the "<Async>" component to resolve:
await asyncCtx.complete();
const page = querySelector<HTMLElement>(view, ".page");
assert(page !== null);
assert(page.innerText === "Hello World!");
});
import { e, mount } from "./rvx.js";
import { Async } from "./rvx.async.js";
import { runAsyncTest, querySelector } from "./rvx.test.js";
await runAsyncTest(async ({ asyncCtx, use }) => {
const view = use(() => {
return mount(
document.body,
Async({
source: async () => {
await something();
return "Hello World!";
},
children: content => e("div").set("class", "page").append(content),
}),
);
});
// Wait for the "<Async>" component to resolve:
await asyncCtx.complete();
const page = querySelector<HTMLElement>(view, ".page");
assert(page !== null);
assert(page.innerText === "Hello World!");
});
Waiting For Expressions
You can watch arbitrary expressions using the watchFor function.
import { $ } from "rvx";
import { watchFor, isPending } from "rvx/async";
// Wait for a specific signal value:
const count = $(0);
setInterval(() => { count.value++ }, 1000);
await watchFor(() => count.value > 7);
// Wait with a timeout:
await watchFor(() => count.value > 7, 500);
// Wait for pending user tasks:
await watchFor(() => !isPending());
import { $ } from "./rvx.js";
import { watchFor, isPending } from "./rvx.async.js";
// Wait for a specific signal value:
const count = $(0);
setInterval(() => { count.value++ }, 1000);
await watchFor(() => count.value > 7);
// Wait with a timeout:
await watchFor(() => count.value > 7, 500);
// Wait for pending user tasks:
await watchFor(() => !isPending());
Polling
You can poll arbitrary functions for the first truthy result using the poll function.
import { poll } from "rvx/test";
// Poll a synchronous function:
const heading = await poll(() => document.querySelector("h1"));
console.log(heading.textContent);
// Poll with a timeout:
await poll(() => { ... }, 100);
// Poll an async function:
await poll(async abortSignal => { ... });
import { poll } from "./rvx.test.js";
// Poll a synchronous function:
const heading = await poll(() => document.querySelector("h1"));
console.log(heading.textContent);
// Poll with a timeout:
await poll(() => { ... }, 100);
// Poll an async function:
await poll(async abortSignal => { ... });
Warning
- Avoid overly expensive computations in the callback as it runs every event cycle.
- Without a timeout,
pollmight run forever.
Concurrency
It is generally possible to run tests for rvx based applications concurrently. However, using APIs that may interfere with each other such as Element.focus can result in flaky tests. To solve this you can use the exclusive function to run code in a globally shared queue for a specific purpose:
import { runAsyncTest, exclusive } from "rvx/test";
const FOCUS_ACTIONS = Symbol("focus actions");
await exclusive(FOCUS_ACTIONS, async () => {
someInput.focus();
await somethingElse();
assert(document.activeElement === someInput);
});
import { runAsyncTest, exclusive } from "./rvx.test.js";
const FOCUS_ACTIONS = Symbol("focus actions");
await exclusive(FOCUS_ACTIONS, async () => {
someInput.focus();
await somethingElse();
assert(document.activeElement === someInput);
});
Using symbols as keys that are in some common place in your test setup is recommended as it prevents any typos in the key, but you can also use anything alse that can be a Map key.