This version of the documentation is outdated, and features documented here may work differently now. You can see the latest stable version of the docs here.

Checkpoints

If you start using Perseus' testing system now, you'll likely hit a snag very quickly, involving errors to do with stale DOM elements. This is an unfortunate side-effect of the way Perseus currently handles initial loads (we move a number of DOM elements around after they've been sent down from the server), which results in the WebDriver thinking half the page has just disappeared out from under it!

This, and many similar problems, are easily solvable using one of Perseus' most powerful testing tools: checkpoints. When you run your app with perseus test, a system is enabled in the background that writes a new DOM element to a hidden list of them when any app code calls checkpoint(). This can then be detected with Fantoccini! Admittedly, a far nicer solution would be DOM events, but the WebDriver protocol doesn't yet support listening for them (understandable since it's mimicking a user's interaction with the browser).

Note that checkpoints will never be reached if your app is not run with perseus test. If you use --no-run and then execute the server binary manually, be sure to provide the PERSEUS_TESTING=true environment variable.

You can wait for a Perseus checkpoint to be reached like so (taken from here):

use fantoccini::{Client, Locator};
use perseus::wait_for_checkpoint;

#[perseus::test]
async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
    c.goto("http://localhost:8080").await?;
    wait_for_checkpoint!("begin", 0, c);
    let url = c.current_url().await?;
    assert!(url.as_ref().starts_with("http://localhost:8080"));

    // The greeting was passed through using build state
    wait_for_checkpoint!("initial_state_present", 0, c);
    wait_for_checkpoint!("page_visible", 0, c);
    let greeting = c.find(Locator::Css("p")).await?.text().await?;
    assert_eq!(greeting, "Hello World!");
    // For some reason, retrieving the inner HTML or text of a `<title>` doens't work
    let title = c.find(Locator::Css("title")).await?.html(false).await?;
    assert!(title.contains("Index Page"));

    // Go to `/about`
    c.find(Locator::Id("about-link")).await?.click().await?;
    let url = c.current_url().await?;
    assert!(url.as_ref().starts_with("http://localhost:8080/about"));
    wait_for_checkpoint!("initial_state_not_present", 0, c);
    wait_for_checkpoint!("page_visible", 1, c);
    // Make sure the hardcoded text there exists
    let text = c.find(Locator::Css("p")).await?.text().await?;
    assert_eq!(text, "About.");
    let title = c.find(Locator::Css("title")).await?.html(false).await?;
    assert!(title.contains("About Page"));
    // Make sure we get initial state if we refresh
    c.refresh().await?;
    wait_for_checkpoint!("initial_state_present", 0, c);

    Ok(())
}

Note in particular the use of the wait_for_checkpoint! macro, which accepts three arguments:

  • Name of the checkpoint
  • Version of the checkpoint
  • Fantoccini client

For want of a better term, that second argument refers to how Perseus manages checkpoints. Because a single checkpoint might be emitted multiple times, Perseus attaches a number to the end of each. The final element id looks like this: __perseus_checkpoint-<checkpoint_name>-<number>, where <number> starts from 0 and increments.

Note: checkpoints are not cleared until the page is fully reloaded, so clicking a link to another page will not clear them!

Custom Checkpoints

In addition to Perseus' internal checkpoints (listed below), you can also use your own checkpoints, though they must follow the following criteria:

  • Must not include hyphens (used as a delimiter character), use underscores instead
  • Must not conflict with an internal Perseus checkpoint name

The best way to uphold the latter of those criteria is to prefix your own checkpoints with something like the name of your app, or even just custom_. Of course, if your app has a name like router, then that will be a problem (many Perseus checkpoints begin with router_), but Perseus will never generate checkpoints internally that begin with custom_.

Note that it's not enough to make sure that your checkpoints don't clash with any existing checkpoints, as new checkpoints may be added in any new release of Perseus, so conflicts may arise with the tiniest of updates!

Internal Checkpoints

Perseus has a number of internal checkpoints that are listed below. Note that this list will increase over time, and potentially in patch releases.

  • begin -- when the Perseus system has been initialized
  • router_entry -- when the Perseus router has reached a verdict and is about to either render a new page, detect the user's locale and redirect, or show an error page
  • not_found -- when the page wasn't found
  • app_shell_entry -- when the page was found and it's being rendered
  • initial_state_present -- when the page has been rendered for the first time, and the server has preloaded everything (see here for details)
  • page_visible -- when the user is able to see page content (but the page isn't interactive yet)
  • page_interactive -- when the page has been hydrated, and is now interactive
  • initial_state_not_present -- when the initial state is not present, and the app shell will need to fetch page data from the server
  • initial_state_error -- when initial state showed an error