Forky

Forky is a monorepo for kickass rust crates.

Crates in this repo are mature enough for usage but not yet at a stage to warrent individual repos.

Very early stage warning:

  • breaking changes on patch versions
  • continued development not guaranteed
  • outdated docs
  • bugs ahoy

Crates

Command Line Interface

Installation

cargo install forky_cli
forky --help

	Welcome to the Forky CLI!
	
	Usage: Forky CLI [COMMAND]
	
	Commands:
	  auto-fs  generate mod and css files
	  watch    execute command on file change
	  serve    serve static files
	  style    Generate types for styles
	  mod      generate mod files for your project
	  sweet    build, serve & run tests in-browser
	  help     Print this message or the help of the given subcommand(s)

watch

Analagous to cargo watch but allows for watch globs.

usage:

forky watch
Usage: forky.exe watch [OPTIONS] <cmd>...

Arguments:
  <cmd>...  the space seperated command to run, ie forky watch -- echo howdy

Options:
  -w, --watch <watch>    paths to watch
  -i, --ignore <ignore>  paths to ignore
      --once             only run once instead of watching indefinitely

Mod

usage:

forky mod

I like to organize projects by many small files and including them becomes a headache so I use a cli to auto-generate mod files that include all files in a directory.

Its current incarnation is zero config and opinionated so you may want to play around with it on an empty project before integrating with existing codebases.

Sweet

Sweet is a full-stack test framework for Rust. Use a single framework for some or all of the supported test types:

TypeDescription
Native - UnitResults-not-panic workflow
Native - E2EFull support for webdriver, fantoccini etc
In-browser - ComponentTest indivudual web components, framework agnostic
In-browser - E2ERun e2e tests on actual elements via iframes

The in-browser tests are architecturally similar to Cypress Component and e2e tests. The native tests may be be compared to the likes of Jest or Playwright.

Features

  • 🔥 Parallel
  • 🕙 Async
  • 🕸️ Native & In-Browser
  • 🌍 E2E Tests
  • ☮️ Intuitive matchers
  • 🌈 Pretty output

Usage

#[sweet_test]
fn true_is_true() -> Result<()> {
  expect(true).to_be_true()
}

Very Quick Start

git clone https://github.com/mrchantey/sweet-demo.git
cd sweet-demo
cargo run --example sweet

Getting Started

Check out the quickstart page or have a browse of the tests written for sweet

Overview

Sweet has four main components:

Quickstart - Native

  1. edit cargo.toml
    [dev-dependencies]
    sweet = # current version here
    
    [[example]]
    name = "sweet"
    path = "test/sweet.rs"
    
  2. create file test/sweet.rs
    #![feature(imported_main)]
    pub use sweet::*;
    
    #[sweet_test]
    fn it_works() -> Result<()>{
    	assert!(true == true);
    	expect(true).to_be_true()?;
    	expect("foobar")
    		.not()
    		.to_start_with("bazz")?;
    	Ok(())
    }
    
  3. run cargo run --example sweet
  4. optional - try changing the above matchers so the test fails ⚡

As an example here is the output of a runner with a few tests:

native-runner

Native Runner

The native runner is an alternative to vanilla rust unit and integration tests. It creates a single binary for all of your tests which speeds up compile times, see this blog for more info.

Usage

The native runner has a few cli options, run with --help to see them all.

cargo run --example sweet --help

Options

  • [match] Space seperated path globs to run, ie my_test or /e2e/
  • -w, --watch Clears screen and does not return error, for use with cargo watch etc
  • -p, --parallel run tests in parallel
  • -s, --silent don't log results

Async Tests

Sweet allows async tests but cannot tell whether all awaited futures are Send.

This is solved by adding the non_send attribute:


// many async functions are parallelizable
#[sweet_test]
async fn example_parrallelizable_test(){
	tokio::time::sleep(Duration::from_millis(100)).await.unwrap();
}


// some must be run on the main thread
#[sweet_test(non_send)]
async fn example_non_send_tests(){
	fantoccini::ClientBuilder::native().connect("http://example.com").await;
	bevy::app::new().add_plugins(DefaultPlugins).run();
}

Web Driver

This section is for native end-to-end tests. For in-browser end-to-end tests see end-to-end.

Sweet can be used with fantoccini or any other webdriver client.

Example

Note the non_send flag, as fantoccini futures are not Send.

use fantoccini::ClientBuilder;
use fantoccini::Locator;
use sweet::*;

#[sweet_test(non_send)]
async fn connects_to_example()->Result<()>{
	let client = ClientBuilder::native()
		.connect("http://localhost:9515").await?;

	client.goto("https://example.com").await?;
	let url = client.current_url().await?;
	expect(url.as_ref()).to_be("https://example.com")?;

	client.close().await?;
	Ok(())
}

Quickstart - Web

  1. Follow native quickstart
  2. Install the cli: cargo install sweet
  3. Run sweet --example my_example
  4. Optional - update your test to do some web stuff:
    web_sys::window().unwrap()
    		.document().unwrap()
    		.body().unwrap()
    		.set_inner_html("<h1>This is a heading</h1>");
    expect(window()).get("h1")?
    	.to_contain_text("This is a heading")?;
    

Here's an example of a runner with a few tests:

wasm-runner

Note: the below noisy warning can be stopped by enabling chrome://flags/#privacy-sandbox-ads-apis

Error with Permissions-Policy header: Origin trial controlled feature not enabled: 'browsing-topics'.

Matchers

expect(element)

Querying a html element is so common Sweet has matchers for some common checks:

  • to_contain_html
  • to_contain_text
  • to_contain_visible_test

Into<HtmlElement>

window().unwrap().document().unwrap().body().unwrap() is a bit of a mouthful 🥴

Sweet provides some wrappers around common types, ie Option<Window>:

//window implements Into<HtmlElement> by getting its document body
expect(web_sys::window()).to_contain_text("sweet as!")?;

//so does iframe
let page = visit("localhost:7777").await;
expect(page).to_contain_text("sweet as!")?;

Async Matchers

Lots of web stuff happens at weird times, so we've got helpers like poll(), which will wait for 2 seconds before failing.

expect(page).poll(|p|
	p.to_contain_text("sweet as!")).await?;

We can also retrieve child elements via polling

expect(page).poll(|p| p.get("div")).await?
	.to_contain_text("sweet as!")?;

End-To-End

This section is for in-browser end-to-end tests. For native end-to-end tests see webdriver.

By default web tests run inside the iframe. This is great for testing components, but when we want to test a page provided by the server we need a different approach.

Test cases marked as e2e will run in the parent process instead. The child iframe can be retrieved via visit(), at which point you can interact with the underlying document just like with unit tests.

Testing iframes from different origins can be tricky, by default a web browser will say "hey, this isnt your site, i won't let you see whats inside iframe.contentDocument etc".

To make this easier, sweet provides a reverse proxy that will serve your url from the same origin. Its been tested on simple sites like these docs, but if you encounter any problems please create an issue.

visit()

visit does three things:

  1. Points the proxy to the provided url
  2. Sets the iframe src to the proxy url
    • fyi this is will be something like /_proxy_/http://localhost:3000
  3. awaits the iframe load event

Example

Here's an example of an end-to-end test running on these docs:

sweet!{
	test e2e "docs origin" {
		let page = visit("http://localhost:3000").await?;
		expect(page)
			.poll(|p|p.to_contain_text("Forky")).await?;
	}
}

And the output looks like this:

end-to-end

Matchers

Matchers are an ergonomic way to make assertions. Providing intellisense and type-specific assertions, they can make for an enjoyable testing experience.

The expect(val) function returns a Matcher<T> where T is the type of the value passed in. What assertions are available for that matcher depend on the

expect(true).to_be_false()?;

Negation

All matchers can be negated by calling not()

expect("foobar").not().to_contain("bazz")?;

Built-in Matchers

Some examples of built-in matchers are:

  • String
    expect("foobar").to_start_with("foo")?;
    
  • Result
      expect(my_result).to_be_ok()?;
    
  • Numbers (ord)
    expect(2).to_be_greater_than(1)?;
    

Extending Matchers

Matchers are easy to extend, particulary using the extend crate.

#![allow(unused)]
fn main() {
use anyhow::Result;
use extend::ext;
use sweet::*;

#[derive(Debug)]
struct Awesomeness(u32);

#[ext]
pub impl Matcher<Awesomeness> {
	fn to_be_more_awesome_than(&self, other:Awesomeness) -> Result<()> {
		let outcome = self.0 > other.0;
		let expected = format!("to be more awesome than {:?}", other);
		self.assert_correct(outcome, &expected)
	}
}
}

Note that here we are calling self.assert_correct() which does to things:

  • checks the outcome is true, or false in the case of negation:
    • expect(foo).not().to_be_more_awesome_than(bar)
  • Formats a pretty backtraced output error if needed.

Sweet CLI

The Sweet CLI is a tool for building, serving & running tests in-browser. It can:

  • Build the tests with cargo build & wasm bindgen
  • Serve the tests on a dev server with live reload
  • Run the tests using chromedriver

Usage:

# headless
sweet --example my_test
# interactive
sweet --example my_test --interactive
# workspaces
sweet --example my_test -p my_crate
# help
sweet --help

Requirements

  • wasm-bindgen-cli
    • cargo install -f wasm-bindgen-cli
  • chromedriver
    • Not required for interactive mode
    •   # windows
        choco install chromedriver
        # mac
        brew install --cask chromedriver
        # linux
        sudo apt install chromium-chromedriver
      
    • If your chrome version gets updated you will need to update chromedriver too:
    • choco upgrade chromedriver etc

Help

sweet --help

Arguments:
  [match]...  filter suites by path glob, ie `my_test` or `/e2e/`

Options:
  -p, --package <package>  pass the --package flag to cargo run
      --release            pass the --release flag to cargo run
      --secure             run the dev server with https
      --static <static>    directory for static files (ie .css) used by component tests
  -w, --watch              live reload file changes
      --headed             run the tests with a visible browser window        
  -i, --interactive        just start the server for viewing in your browser 

Macros

#[sweet_test]

Tests can be declared via an attribute.

#[sweet_test]
fn foobar(){}

//accepts several flags, async functions or an `anyhow::Result` return type
#[sweet_test(skip,only,e2e,non_send)]
async fn foobar()->Result<()>{
	expect(true).to_be_true()
}

sweet!

A layout more familiar to front-end developers. Note that rust formatters may not indent etc. the contents of this macro correctly.

sweet!{
	it "has less boilerplate" {
		expect(true).to_be_true()?;
	}
	test "is an alias for it"{}
	it skip "wont run"{}
	it only "will exclude non-onlys in this suite"{}
	it e2e "(in-browser) runs in the parent process"{}
}

Workspaces

Workspaces are fully supported by Sweet, here's a few notes:

Duplicate Binaries

As per this PR crates that contain identical example names is not supported. For this reason it is recommended to update your Cargo.toml.

[[example]]
- name = "sweet"
+ name = "test_crate_a"
path = "test/sweet.rs"

This will achieve two things:

  • Avoid weird bugs where running crate_a actually runs crate_b
  • Reduce unneseccary recompilation.

Running cargo run -p crate_a --example sweet_crate_a is a bit of a mouthfull, I solve this with just:

#justfile
test crate *args:
	cargo run -p {{crate}} --example sweet_{{crate}} -- {{args}}

Now you can run: just test crate_a

CI / CD

Sweet has full CI/CD support for all test types. In fact, the tests for this repo are all run using Github Actions.

An example workflow may look something like this:

name: 🔎 Test Crates
on:
  push:
    branches: main
  pull_request:
    branches: main
env:
  CARGO_TERM_COLOR: always
jobs:
  build_and_test:
    name: Build and Test
    runs-on: ubuntu-latest
    steps:
    - name: 📂 Checkout
      uses: actions/checkout@v3
    - name: 📂 Cache
      uses: actions/cache@v3
      with:
        path: |
          ~/.cargo/bin/
          ~/.cargo/registry/index/
          ~/.cargo/registry/cache/
          ~/.cargo/git/db/
          target/
        key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
    - name: 🔨 Install Chromedriver
      uses: nanasess/setup-chromedriver@v2
    - name: 🔨 Install Rust
      uses: actions-rs/toolchain@v1
      with:
        toolchain: nightly
        override: true
        default: true
    - name: 🔨 Install Wasm Target
      run: rustup target add wasm32-unknown-unknown
    - name: 🔨 Install Wasm Bindgen
      uses: baptiste0928/cargo-install@v2
      with:
        crate: wasm-bindgen-cli
        version: '0.2.87' # TODO ensure this matches your wasm-bindgen version
    - name: 🔨 Install Sweet Cli
      uses: baptiste0928/cargo-install@v2
      with:
        crate: sweet-cli
    - name: 🔨 Build
      run: cargo build
    - name: 🔎 Test Native
      run: cargo run --example sweet
    - name: 🔎 Test Wasm
      run: sweet --example sweet

FAQ

Why use [[example]] instead of [[test]]

This makes it easier for the wasm test runner to produce cleaner output, but if you're only running native tests feel free to use [[test]] with harness=false.

What about wasm-bindgen-test?

Sweet has different priorities from wasm-bindgen-test in its current state, namely a focus on UI & interactivity.

Changelog

TODO

  • Kill long-running tests
  • Display typename in failed tests: tests::foo::works

main

  • rename bevy_ecs feature to bevy
  • move time extensions to bevy feature

0.1.33 - 17/10/2023

  • modularity & improved documentation

0.1.32 - 13/10/2023

  • attribute macros

0.1.31 - 13/09/2023

  • Headless in-browser tests
  • ci/cd workflows

0.1.19 - 10/08/2023

  • End-to-end testing
  • Nicer async ergonomics: matcher.poll().await?

0.1.18 - 03/08/2023

  • Component Testing
  • Native Testing
  • Parallel
  • Async
  • Wasm Runner

Contributing

If this project is of interest to you feel free to have a dig around!

Everything is very early days so there are no doubt lots of bugs and missing features. If you find something that could be improved please open an issue or PR.

Getting Started

Most of this is for my own reference, but you may find it useful:

  1. Install Rust
    • Installer
    • Use powershell and be sure to carefully follow steps for build tools
  2. Install Depedencies
    •   	choco install just
        	choco install cygwin
        	# check
        	just all check
        	# tools
        	cargo install cargo-watch cargo-edit
        	rustup toolchain install nightly
        	rustup component add rustfmt --toolchain nightly
        	cargo +nightly fmt
        	rustup default nightly
        	# test - compilation will take several minutes
        	just all test
      

Cygwin

Justfiles require cygwin to work on windows.

  1. install cygwin
  2. add to path: C:\tools\cygwin\bin

Wasm

  • follow the bevy guide
  • setup
    rustup target install wasm32-unknown-unknown
    cargo install wasm-server-runner
    cargo install -f wasm-bindgen-cli
    #.cargo/config.toml
    [target.wasm32-unknown-unknown]
    runner = "wasm-server-runner"
    
  • run
    #run
    cargo run -p forky_play --example maze --target wasm32-unknown-unknown
    #compile
    cargo build -p forky_play --example maze --release --target wasm32-unknown-unknown
    #build bindings
    wasm-bindgen --out-dir ./html/maze --target web ./target/wasm32-unknown-unknown/release/examples/maze.wasm
    cd html && live-server