You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(testing): ✨ added testing harness and tests for examples (#21)
* feat(testing): ✨ added testing mode and checkpoint queue
* feat(testing): ✨ added `perseus::test` proc macro
* feat(cli): ✨ added `--no-run` option to cli
This avoids running the server, and instead prints the executable.
* refactor(testing): ♻️ made `test` macro only run wasm tests if `PERSEUS_RUN_WASM_TESTS` is set
Avoids needing to exclude tests for larger workspaces.
* chore(testing): 🔧 updated bonnie conf for testing
* docs: 📝 updated CONTRIBUTING for new testing arch
* feat(testing): ✨ updated testing proc macro to support headless browsers
Use `PERSEUS_RUN_WASM_TESTS_HEADLESS`.
* style: 🎨 ran `cargo fmt`
* chore: 🔧 updated `bonnie check` to run wasm tests
* ci: 👷 updated ci to use bonnie
* chore(testing): 🐛 made test script exit with correct code
Fixes CI false positives.
* ci: 👷 added geckodriver support to ci
We'll see if this works...
* ci: 💚 fixed ci firefox/geckodriver installation
* ci: 💚 fixed typo in ci workflow
This may go on for a while.
* ci: 💚 fixed ci build again
* ci: 💚 installed `wasm-pack` on ci
* refactor(testing): 🚚 renamed `shell_entry` checkpoint to `begin`
* feat(testing): ✨ added `wait_for_checkpoint!` macro
* chore(examples): 🚚 renamed `basic` example to `perseus-example-basic`
Was `perseus-example-cli` from legacy.
* feat(testing): ✨ added `page_visible` checkpoint and renamed `page_hydrated` to `page_interactive`
* test: ✅ added tests for basic example
* test: ✅ added tests for i18n example
Also made basic example's more resilient.
* fix(routing): 🐛 escaped double quotes in initial state global variable
In some cases, not escaping could cause a runtime panic.
* fix(testing): 🐛 made tests run sequentially
Otherwise `geckodriver` fails.
* test: ✅ added tests for showcase example
Also added an illegal route to `/timeisr` for testing.
* docs(book): 📝 wrote docs on testing
* ci: 🙈 updated gitignore in basic example
Should now preserve needed folder structure for CI.
* style: 🎨 ran `cargo fmt`
* ci: 💚 fixed ci build folder structure issues
* ci: 💚 fixed ci build
* ci: 👷 updated ci to test all examples
* ci: 👷 updated ci to only deploy book from `main`
* docs(book): 📝 merged testing docs into 0.2.x
Copy file name to clipboardExpand all lines: CONTRIBUTING.md
+19-9
Original file line number
Diff line number
Diff line change
@@ -18,23 +18,33 @@ Make sure your code doesn't break anything existing, that all tests pass, and, i
18
18
19
19
After you've submitted a pull request, a maintainer will review your changes. Unfortunately, not every pull request will be merged, but we'll try to request changes so that your pull request can best be integrated into the project.
20
20
21
-
## Building and Testing
21
+
## Building
22
22
23
-
Perseus uses [Bonnie](https://github.com/arctic-hen7/bonnie) for command aliasing, and most of the project work is done on the `showcase` example, which is used for live development testing.
23
+
Perseus uses [Bonnie](https://github.com/arctic-hen7/bonnie) for command aliasing (you can install it with `cargo install bonnie`), and most of the project testing is done in the `examples` directory. You can run `bonnie help` to see all available commands, but this is the one you'll use the most:
24
24
25
-
- Terminal 1
26
-
- `cd examples/showcase`
27
-
- `bonnie build --watch`
28
-
- Terminal 2
29
-
- `cd examples/showcase`
30
-
- `bonnie serve`
25
+
-`bonnie dev example showcase serve` -- serves the `showcase` example to <http://localhost:8080>
31
26
32
-
Now you can make changes to the core library and enjoy! Nearly all project commands are managed with Bonnie, and you can see what everything does by checking out the various `bonnie.toml` files throughout the project!
27
+
## Testing
28
+
29
+
Nearly all Perseus' tests are end-to-end, and run using the Perseus test macro for each example (under `examples`). You can run all tests with `bonnie test`, provided that you're running a WebDriver instance at <http://localhost:4444>.
30
+
31
+
If you're new to WebDriver, install `geckodriver` and Firefox, and then run `geckodriver` in another terminal. Then all Perseus tests will run fine.
32
+
33
+
You can also run a full check on all your code with `bonnie check`, which is the same as what's performed on CI.
33
34
34
35
## Documentation
35
36
36
37
If the code you write needs to be documented in, the README, the book, or elsewhere, please do so! Also, **please ensure your code is commented**, it makes everything so much easier.
37
38
39
+
## Branches
40
+
41
+
Perseus uses a relatively intuitive branching system:
42
+
43
+
-`main` -- the rolling-release version of the project, which should not be committed to directly
44
+
-`stable` -- the stable version of the project, which should reflect released features (should be in line with latest tag)
45
+
46
+
A separate branch is created for new features/fixes, which are then merged into `main` with pull requests. Note that new releases can only be authored from the `stable` branch (checked by Bonnie).
47
+
38
48
## Committing
39
49
40
50
We use the Conventional Commits system, but you can commit however you want. Your pull request will be squashed and merged into a single compliant commit, so don't worry about this!
"cargo publish --allow-dirty %%", # Without this flag, `.perseus` will be a problem because it's not in Git
82
115
# We delay this so that `crates.io` can have time to host the core
83
116
"cd ../perseus-actix-web",
84
117
"cargo publish %%"
85
118
]
86
-
publish.desc = "publishes all packages to crates.io"
119
+
publish.desc = "publishes all packages to crates.io (needs branch 'stable')"
120
+
121
+
check-branch.cmd.exec = "[[ $(git rev-parse --abbrev-ref HEAD) == \"%branch\" ]] && exit 0 || echo \"You need to be on Git branch '%branch' to run this command.\"; exit 1"
check-branch.desc = "checks if the current git branch is the given argument, signals with exit codes (and warns), this will prevent following commands from running if it fails"
Copy file name to clipboardExpand all lines: docs/0.2.x/src/config-managers.md
+1-1
Original file line number
Diff line number
Diff line change
@@ -2,4 +2,4 @@
2
2
3
3
Perseus generates a number of files in its build process, which allow it to make your app extremely performant on the client and the server. By default, these are stored under `.perseus/dist/`, however there may be cases in which you want to store these files elsewhere, particularly given that **they need to modified at runtime**. While not recommended, it may be necessary in some deployments to move these files to a database or CMS. Note that this will have a notable performance impact on your app, and the default is always recommended.
4
4
5
-
That said, Perseus allows you to store content wherever you'd like with the `ConfigManager` trait, the default implementation of which is `FsConfigManager`, which takes a directory to use. Very similar to the [translations manager]() system, you can customize the options to this by providing your own instance of it under `define_app!`'s `config_manager` property, or you can build your own for interfacing with a non-filesystem storage apparatus. To do the latte, you'll need to consult [this file](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/config_manager.rs), and, if you're stuck, don't hesitate to ask a question under [discussions](https://github.com/arctic-hen7/perseus/discussions/new) on GitHub!
5
+
That said, Perseus allows you to store content wherever you'd like with the `ConfigManager` trait, the default implementation of which is `FsConfigManager`, which takes a directory to use. Very similar to the [translations manager]() system, you can customize the options to this by providing your own instance of it under `define_app!`'s `config_manager` property, or you can build your own for interfacing with a non-filesystem storage apparatus. To do the latter, you'll need to consult [this file](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/config_manager.rs), and, if you're stuck, don't hesitate to ask a question under [discussions](https://github.com/arctic-hen7/perseus/discussions/new) on GitHub!
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!
4
+
5
+
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).
6
+
7
+
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.
8
+
9
+
You can wait for a Perseus checkpoint to be reached like so (taken from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/basic/tests/main.rs)):
Note in particular the use of the `wait_for_checkpoint!` macro, which accepts three arguments:
16
+
17
+
- Name of the checkpoint
18
+
- Version of the checkpoint
19
+
- Fantoccini client
20
+
21
+
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.
22
+
23
+
*Note: checkpoints are not cleared until the page is fully reloaded, so clicking a link to another page will not clear them!*
24
+
25
+
## Custom Checkpoints
26
+
27
+
In addition to Perseus' internal checkpoints (listed below), you can also use your own checkpoints, though they must follow the following criteria:
28
+
29
+
- Must not include hyphens (used as a delimiter character), use underscores instead
30
+
- Must not conflict with an internal Perseus checkpoint name
31
+
32
+
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_`.
33
+
34
+
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!
35
+
36
+
## Internal Checkpoints
37
+
38
+
Perseus has a number of internal checkpoints that are listed below. Note that this list will increase over time, and potentially in patch releases.
39
+
40
+
-`begin` -- when the Perseus system has been initialized
41
+
-`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
42
+
-`not_found` -- when the page wasn't found
43
+
-`app_shell_entry` -- when the page was found and it's being rendered
44
+
-`initial_state_present` -- when the page has been rendered for the first time, and the server has preloaded everything (see [here](../advanced/initial-loads.md) for details)
45
+
-`page_visible` -- when the user is able to see page content (but the page isn't interactive yet)
46
+
-`page_interactive` -- when the page has been hydrated, and is now interactive
47
+
-`initial_state_not_present` -- when the initial state is not present, and the app shell will need to fetch page data from the server
48
+
-`initial_state_error` -- when initial state showed an error
Now that you know a bit more about how Perseus tests work, it's time to go through how to write them!
4
+
5
+
Remember, you're controlling an actual browser, so you basically have everything available to you that a user can do (mostly). You can even take screenshots! All this is achieved with [Fantoccini](https://github.com/jonhoo/fantoccini), which you can learn more about [here](https://docs.rs/fantoccini). For now though, here's a quick tutorial on the basics, using [this](https://github.com/arctic-hen7/perseus/blob/main/examples/basic/tests/main.rs) example:
You can trivially go to a page of your app by running `c.goto("...")`. The above example ensures that the URL is valid, but you shouldn't have to do this unless you're testing a page that automatically redirects the user. Also, if you're using [i18n](../i18n/intro.md), don't worry about testing automatic locale redirection, we've already done that for you!
14
+
15
+
Once you've arrived at a page, you should wait for the `router_entry` (this example uses `begin` because it tests internal parts of Perseus) checkpoint, which will be reached when Perseus has decided what to do with your app. If you're testing particular page logic, you should wait instead for `page_visible`, which will be reached when the user could see content on your page, and then for `page_interactive`, which will be reached when the page is completely ready. Remember though, you only need to wait for the checkpoints that you actually use (e.g. you don't need to wait for `page_visible` and `page_interactive` if you're not doing anything in between).
16
+
17
+
## Finding an Element
18
+
19
+
You can find an element easily by using Fantoccini's `Locator``enum`. This has two options, `Id` or `Css`. The former will find an element by its HTML `id`, and the latter will use a CSS selector ([here](https://www.w3schools.com/cssref/css_selectors.asp)'s a list of them). In the above example, we've used `Locator::Css("p")` to get all paragraph elements, and then we've plugged that into `c.find()` to get the first one. Then, we can get its `innerText` with `.text()` and assert that is what we want it to be.
20
+
21
+
### Caveats
22
+
23
+
As you may have noticed above, asserting on the contents of a `<title>` is extremely unintuitive, as it requires using `.html(false)` (meaning include the element tag itself) and asserting against that. For some reason, neither `.html(true)` nor `.text()` return anything. There's a tracking issue for this [here](https://github.com/jonhoo/fantoccini/issues/136).
24
+
25
+
## Miscellaneous
26
+
27
+
For full documentation of how Fantoccini works, see its API documentation [here](https://docs.rs/fantoccini).
0 commit comments