Skip to content

Commit

Permalink
Merge pull request #306 from redbadger/no-apptester
Browse files Browse the repository at this point in the history
Update tests in the counter example to not use AppTester
  • Loading branch information
StuartHarris authored Jan 27, 2025
2 parents 7cbb961 + f785a0b commit 78f1aaa
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 195 deletions.
6 changes: 3 additions & 3 deletions docs/src/getting_started/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ keywords = ["crux", "crux_core", "cross-platform-ui", "ffi", "wasm"]
rust-version = "1.66"

[workspace.dependencies]
anyhow = "1.0.86"
crux_core = "0.8"
serde = "1.0.203"
anyhow = "1.0.95"
crux_core = "0.11.1"
serde = "1.0.217"
```

The library's manifest, at `/shared/Cargo.toml`, should look something like the
Expand Down
87 changes: 59 additions & 28 deletions docs/src/guide/hello_world.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,20 +370,25 @@ side-effect.
That gives us the following update function, with some placeholders:

```rust,noplayground
fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
fn update(
&self,
event: Self::Event,
model: &mut Self::Model,
_caps: &Self::Capabilities,
) -> Command<Effect, Event> {
match event {
Event::Get => {
// TODO "GET /"
}
Event::Set(_response) => {
// TODO Get the data and update the model
caps.render.render();
render::render()
}
Event::Increment => {
// optimistic update
model.count.value += 1;
model.count.updated_at = None;
caps.render.render();
render::render()
// real update
// TODO "POST /inc"
Expand All @@ -392,7 +397,7 @@ fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capab
// optimistic update
model.count.value -= 1;
model.count.updated_at = None;
caps.render.render();
render::render()
// real update
// TODO "POST /dec"
Expand Down Expand Up @@ -432,7 +437,13 @@ sent by the Shell across the FFI boundary, which is the reason for the need to
serialize in the first place — in a way, it is private to the Core.

Finally, let's get rid of those TODOs. We'll need to add crux_http in the
`Capabilities` type, so that the `update` function has access to it:
`Capabilities` type, so that the `update` function has access to it.

```admonish note
In the latest versions of `crux_http` (>= `v0.11.0`), this `Capabilities` type
id being deprecated in favour of the new `Command` API (see the description of
[Managed Effects](./effects.md) for more details).
```

```rust,noplayground
use crux_http::Http;
Expand All @@ -450,22 +461,30 @@ anyone. Later on, we'll also see that Crux apps [compose](composing.md), relying
on each app's `Capabilities` type to declare its needs, and making sure the
necessary capabilities exist in the parent app.

We can now implement those TODOs, so lets do it.
We can now implement those TODOs, so lets do it. We're using the latest `Command` API
and so the `update` function will return a `Command` that has been created by
the `crux_http` and `render` capabilities (rather than using the `caps` parameter
in the method signature):

```rust,noplayground
const API_URL: &str = "https://crux-counter.fly.dev";
//...
fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
match event {
Event::Get => {
caps.http.get(API_URL).expect_json().send(Event::Set);
}
fn update(
&self,
event: Self::Event,
model: &mut Self::Model,
_caps: &Self::Capabilities,
) -> Command<Effect, Event> { match event {
Event::Get => Http::get(API_URL)
.expect_json()
.build()
.then_send(Event::Set),
Event::Set(Ok(mut response)) => {
let count = response.take_body().unwrap();
model.count = count;
caps.render.render();
render::render()
}
Event::Set(Err(_)) => {
panic!("Oh no something went wrong");
Expand All @@ -476,32 +495,36 @@ fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capab
value: model.count.value + 1,
updated_at: None,
};
caps.render.render();
// real update
let base = Url::parse(API_URL).unwrap();
let url = base.join("/inc").unwrap();
caps.http.post(url).expect_json().send(Event::Set);
let call_api = {
let base = Url::parse(API_URL).unwrap();
let url = base.join("/inc").unwrap();
Http::post(url).expect_json().build().then_send(Event::Set)
};
render().and(call_api)
}
Event::Decrement => {
// optimistic update
model.count = Count {
value: model.count.value - 1,
updated_at: None,
};
caps.render.render();
// real update
let base = Url::parse(API_URL).unwrap();
let url = base.join("/dec").unwrap();
caps.http.post(url).expect_json().send(Event::Set);
let call_api = {
let base = Url::parse(API_URL).unwrap();
let url = base.join("/dec").unwrap();
Http::post(url).expect_json().build().then_send(Event::Set)
};
render().and(call_api)
}
}
}
```

There's a few things of note. The first one is that the `.send` API at the end
There's a few things of note. The first one is that the `.then_send` API at the end
of each chain of calls to `crux_http` expects a function that wraps its argument
(a `Result` of a http response) in a variant of `Event`. Fortunately, enum tuple
variants create just such a function, and we can use it. The way to read the
Expand All @@ -511,21 +534,29 @@ result". Interestingly, we didn't need to specifically mention the `Count` type,
as the type inference from the `Event::Set` variant is enough, making it really
easy to read.

The other thing of note is that the capability calls don't block. They queue up
The other thing of note is that the Commands don't block. They queue up
requests to send to the shell and execution continues immediately. The requests
will be sent in the order they were queued and the asynchronous execution is the
job of the shell.

You can find the the complete example, including the shell implementations
You can find the the complete example, including the tests and shell implementations
[in the Crux repo](https://github.com/redbadger/crux/blob/master/examples/counter/).
It's interesting to take a closer look at the unit tests
It's interesting to take a closer look at the unit tests:

```admonish note
These tests are taken from the Counter example
[implementation](https://github.com/redbadger/crux/blob/master/examples/counter/shared/src/app.rs)
where we delegate to our own update function that does not take the `Capabilities`
parameter, allowing us to test the app directly, without having to rely on
the `AppTester`.
```

```rust,noplayground
{{#include ../../../examples/counter/shared/src/app.rs:simple_tests}}
```

Incidentally, we're using [`insta`](https://crates.io/crates/insta) in that last
test to assert that the view model is correct. If you don't know it already,
For bigger `Model` or `ViewModel` structs, it may be easier to assert their correctness with
[`insta`](https://crates.io/crates/insta). If you don't know it already,
check it out. The really cool thing is that if the test fails, it shows you a
diff of the actual and expected output, and if you're happy with the new output,
you can accept the change (or not) by running `cargo insta review` — it will
Expand Down
Loading

0 comments on commit 78f1aaa

Please sign in to comment.